"""
Web-based Annotation Interface for Privacy Inferences
Modified for Hugging Face Spaces deployment
Run: python annotation_app.py
Then open: http://localhost:7860
"""
from flask import Flask, render_template, request, jsonify, send_file, session, redirect, url_for
import json
from pathlib import Path
from datetime import datetime
import os
from functools import wraps
import zipfile
import io
from collections import defaultdict
app = Flask(__name__)
# Security configuration
# Configuration Flask
# Security configuration - Simplified for HF Spaces
app.secret_key = os.environ.get('SECRET_KEY', 'votre-cle-secrete-tres-longue-a-changer-123456789')
# Configuration (ligne ~28)
RESULTS_DIR = Path("results")
ANNOTATIONS_DIR = Path("annotations")
ANNOTATIONS_DIR.mkdir(exist_ok=True)
MONTHS = [
'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE',
'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER'
]
CATEGORIES = ['health', 'religion', 'family', 'routines', 'work', 'leisure', 'economics']
# Admin password - DOIT รTRE DรFINI AVANT LE DEBUG
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'antoine2025')
# Debug: Print config at startup - MAINTENANT C'EST BON
print("="*70)
print("๐ AUTHENTICATION CONFIG")
print("="*70)
print(f"SECRET_KEY configured: {'Yes' if app.config.get('SECRET_KEY') else 'No'}")
print(f"ADMIN_PASSWORD configured: {'Yes' if ADMIN_PASSWORD else 'No'}")
print(f"Session cookie secure: {app.config.get('SESSION_COOKIE_SECURE')}")
print("="*70)
# ============================================================================
# AUTHENTICATION
# ============================================================================
def login_required(f):
"""Decorator to protect routes"""
@wraps(f)
def decorated_function(*args, **kwargs):
# Check session AND cookie
if not session.get('logged_in') and request.cookies.get('auth_token') != 'authenticated':
return redirect(url_for('login'))
# If cookie is valid but session is empty, restore session
if not session.get('logged_in') and request.cookies.get('auth_token') == 'authenticated':
session['logged_in'] = True
return f(*args, **kwargs)
return decorated_function
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login page"""
# If already logged in, redirect to index
if session.get('logged_in') or request.cookies.get('auth_token') == 'authenticated':
return redirect(url_for('index'))
if request.method == 'POST':
password = request.form.get('password', '')
print(f"๐ Login attempt") # Debug
if password == ADMIN_PASSWORD:
session['logged_in'] = True
print("โ
Login successful! Setting cookie...") # Debug
# Create response with redirect
response = redirect(url_for('index'))
# Set a persistent cookie as backup
response.set_cookie(
'auth_token',
'authenticated',
max_age=3600, # 1 hour
httponly=True,
samesite='Lax'
)
return response
else:
print("โ Login failed - wrong password") # Debug
error_message = "Mot de passe incorrect"
else:
error_message = None
# GET request or failed login - show login form
return f'''
Connexion - Privacy Annotation
๐ Privacy Annotation
{'
' + error_message + '
' if error_message else ''}
Interface d'annotation pour รฉvaluation des modรจles LLM
'''
@app.route('/logout')
def logout():
"""Logout"""
session.clear()
response = redirect(url_for('login'))
response.set_cookie('auth_token', '', max_age=0) # Delete cookie
return response
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def get_available_models():
"""Get list of available model directories."""
if not RESULTS_DIR.exists():
return []
return [d.name for d in RESULTS_DIR.iterdir() if d.is_dir() and not d.name.startswith('.')]
def load_result(model_name, month):
"""Load a specific result file."""
filepath = RESULTS_DIR / model_name / "2017" / f"2017_{month}_P1.json"
if not filepath.exists():
return None
with open(filepath, 'r') as f:
return json.load(f)
def get_annotation_status():
"""Get status of all annotations."""
status = {}
models = get_available_models()
for model in models:
status[model] = {}
for month in MONTHS:
ann_file = ANNOTATIONS_DIR / f"{model}_{month}_annotations.json"
status[model][month] = ann_file.exists()
return status
def extract_inferences(response, category):
"""Extract inferences for a specific category."""
inferences = []
lines = response.split('\n')
in_category = False
for line in lines:
# Check if entering this category
if category.lower() in line.lower() and (':' in line or category in line):
in_category = True
continue
# Check if entering new category
if any(cat.lower() in line.lower() for cat in CATEGORIES if cat != category):
in_category = False
# Extract inference
if in_category and line.strip():
cleaned = line.strip().lstrip('โข-*0123456789.) ')
if len(cleaned) > 15:
inferences.append(cleaned)
return inferences
# ============================================================================
# MAIN ROUTES
# ============================================================================
@app.route('/')
# @login_required # โ COMMENTร TEMPORAIREMENT POUR DEBUG
def index():
"""Main page - show annotation dashboard."""
try:
models = get_available_models()
status = get_annotation_status()
print(f"๐ Models found: {models}") # Debug
print(f"๐ Status: {status}") # Debug
return render_template('index.html',
models=models,
months=MONTHS,
status=status)
except Exception as e:
print(f"โ ERROR in index(): {e}") # Debug
import traceback
traceback.print_exc()
return f"Error loading dashboard
{e}{traceback.format_exc()}", 500
@app.route('/annotate//')
#@login_required
def annotate(model, month):
"""Annotation page for specific model and month - Simplified version"""
print(f"๐ Annotate called: model={model}, month={month}")
# Load result
result = load_result(model, month)
if not result:
print(f"โ Result not found for {model}/{month}")
return f"Error: Result not found for {model}/{month}
", 404
response_text = result.get('response', '')
print(f"โ
Result loaded. Response length: {len(response_text)} chars")
# Check if already annotated
ann_file = ANNOTATIONS_DIR / f"{model}_{month}_annotations.json"
existing_annotation = None
if ann_file.exists():
with open(ann_file, 'r') as f:
existing_annotation = json.load(f)
# Extract inferences by category
category_inferences = {}
for category in CATEGORIES:
inferences = extract_inferences(response_text, category)
category_inferences[category] = inferences
print(f" {category}: {len(inferences)} inferences")
# Count annotations
annotation_count = 0
if existing_annotation:
for cat_anns in existing_annotation.get('annotations', {}).values():
annotation_count += len(cat_anns)
# Build HTML directly (no template needed)
html = f'''
{model.upper()} - {month} 2017
๐ LLM Response
Response: {len(response_text)} characters
{response_text}
โ๏ธ Annotate Inferences
{annotation_count} / {sum(len(infs) for infs in category_inferences.values())} annotated
'''
return html
@app.route('/api/save_annotation', methods=['POST'])
#@login_required
def save_annotation():
"""Save annotation via API."""
data = request.json
model = data['model']
month = data['month']
annotations = data['annotations']
# Load original result
result = load_result(model, month)
if not result:
return jsonify({'success': False, 'error': 'Result not found'}), 404
# Create annotation data
ann_data = {
'model_name': model,
'month': month,
'year': 2017,
'annotated_at': datetime.now().isoformat(),
'annotation_mode': 'web_interface',
'original_response': result['response'],
'annotations': annotations,
'metadata': {
'trajectory_period': result.get('trajectory_period'),
'prompt_type': result.get('prompt_type'),
}
}
# Save to file
ann_file = ANNOTATIONS_DIR / f"{model}_{month}_annotations.json"
with open(ann_file, 'w') as f:
json.dump(ann_data, f, indent=2)
return jsonify({'success': True, 'file': str(ann_file)})
@app.route('/api/metrics/')
#@login_required
def get_metrics(model):
"""Calculate metrics for a model."""
pattern = f"{model}_*_annotations.json"
ann_files = list(ANNOTATIONS_DIR.glob(pattern))
if not ann_files:
return jsonify({'success': False, 'error': 'No annotations found'})
total_tp = 0
total_fp = 0
total_pa = 0
by_category = {}
annotated_months = []
for ann_file in ann_files:
with open(ann_file, 'r') as f:
data = json.load(f)
annotated_months.append(data['month'])
for category, items in data['annotations'].items():
if category not in by_category:
by_category[category] = {'TP': 0, 'FP': 0, 'PA': 0}
for item in items:
label = item['label']
if label == 'TP':
total_tp += 1
by_category[category]['TP'] += 1
elif label == 'FP':
total_fp += 1
by_category[category]['FP'] += 1
elif label == 'PA':
total_pa += 1
by_category[category]['PA'] += 1
# Calculate precision
precision = total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0
# Category metrics
category_metrics = {}
for category, stats in by_category.items():
tp = stats['TP']
fp = stats['FP']
prec = tp / (tp + fp) if (tp + fp) > 0 else 0
category_metrics[category] = {
'tp': tp,
'fp': fp,
'pa': stats['PA'],
'precision': round(prec, 3)
}
return jsonify({
'success': True,
'model': model,
'months_annotated': len(annotated_months),
'annotated_months': annotated_months,
'total_tp': total_tp,
'total_fp': total_fp,
'total_pa': total_pa,
'precision': round(precision, 3),
'by_category': category_metrics
})
@app.route('/metrics')
#@login_required
def metrics_page():
"""Metrics dashboard page."""
models = get_available_models()
return render_template('metrics.html', models=models)
@app.route('/download-annotations')
#@login_required
def download_annotations():
"""Download all annotations as ZIP file"""
memory_file = io.BytesIO()
# Create ZIP with all annotations
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf:
ann_files = list(ANNOTATIONS_DIR.glob('*.json'))
if not ann_files:
return "Aucune annotation disponible pour le moment.", 404
for file in ann_files:
zf.write(file, file.name)
memory_file.seek(0)
# Filename with date
filename = f'annotations_antoine_{datetime.now().strftime("%Y%m%d_%H%M")}.zip'
return send_file(
memory_file,
mimetype='application/zip',
as_attachment=True,
download_name=filename
)
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
# Hugging Face Spaces uses port 7860 by default
port = int(os.environ.get('PORT', 7860))
# Create directories if needed
templates_dir = Path("templates")
templates_dir.mkdir(exist_ok=True)
RESULTS_DIR.mkdir(exist_ok=True)
ANNOTATIONS_DIR.mkdir(exist_ok=True)
print("="*70)
print("PRIVACY INFERENCE ANNOTATION WEB INTERFACE")
print("="*70)
print(f"\nโ Starting server on port {port}...")
print(f"โ Results directory: {RESULTS_DIR.absolute()}")
print(f"โ Annotations will be saved to: {ANNOTATIONS_DIR.absolute()}")
# Check if running on HF
if os.environ.get('SPACE_ID'):
print(f"โ Running on Hugging Face Spaces")
print(f"โ Space ID: {os.environ.get('SPACE_ID')}")
else:
print(f"\n๐ Open your browser to: http://localhost:{port}")
print("\nPress Ctrl+C to stop the server")
print("="*70 + "\n")
# IMPORTANT: host='0.0.0.0' to be accessible from outside
# debug=False for production
app.run(host='0.0.0.0', port=port, debug=False)