## 2. Algorithme de recommandation

### 2.1 Classification de géo-pertinence

**Décision** : 3 types de contenus selon leur pertinence géographique

| Type | Description | Exemple | Pondération géo |
|------|-------------|---------|-----------------|
| **Géo-ancré** | Contenu lié à un lieu précis | Audio-guide monument, pub restaurant local | 70% |
| **Géo-contextuel** | Pertinent dans une zone | Actualité régionale, événement local | 50% |
| **Géo-neutre** | Universel, pas de lien géo | Podcast philosophie, musique | 20% |

**Qui décide** :
- ✅ Créateur choisit le type à la publication
- ✅ Modération peut reclassifier après validation
- ✅ Modification possible après publication (tout le monde a le droit de se tromper)

**Justification** :
- Différencie audio-guide (hyper-local) des podcasts génériques
- Algorithme adapte automatiquement la pondération
- Coût : champ supplémentaire en DB + règle algo

---

### 2.2 Formule de scoring

**Décision** : Score combiné dynamique selon type de contenu

```
score_final = (score_geo * poids_geo_type)
            + (score_interets * poids_interets_type)
            + (score_engagement * 0.2)
            + (bonus_aleatoire)

où :
- score_geo = 1 - (distance_km / distance_max_km)
- score_interets = moyenne des jauges utilisateur pour les tags du contenu
- score_engagement = (taux_completion * 0.5) + (ratio_likes * 0.3) + (ratio_abonnements * 0.2)
- bonus_aleatoire = 10% des recommandations tirées aléatoirement
```

#### Calcul détaillé du score_interets

**Domaine des données** :
- Jauges utilisateur : stockées en pourcentage [0-100]
- score_interets : normalisé dans l'intervalle [0.0-1.0] pour pondération

**Formule exacte** :
```
score_interets = (SUM(gauge_values_for_tags) / NB_TAGS) / 100

où :
- gauge_values_for_tags = valeurs des jauges correspondant aux tags du contenu
- NB_TAGS = nombre de tags du contenu (minimum 1, maximum 3)
- Division par 100 pour normaliser [0-100] → [0.0-1.0]
```

**Exemple concret** :
```
Contenu : "Visite du Louvre"
Tags : ["Musique", "Tourisme"]

Utilisateur :
- Jauge "Musique" = 75%
- Jauge "Tourisme" = 60%
- Jauge "Automobile" = 40% (non pertinente, ignorée)

Calcul :
score_interets = ((75 + 60) / 2) / 100
               = (135 / 2) / 100
               = 67.5 / 100
               = 0.675

Impact dans le scoring final (type géo-contextuel) :
score_final = (score_geo * 0.5) + (score_interets * 0.3) + (score_engagement * 0.2) + bonus_aleatoire
            = (0.8 * 0.5) + (0.675 * 0.3) + (0.5 * 0.2) + 0
            = 0.4 + 0.2025 + 0.1
            = 0.7025 / 1.0
```

**Cas limites** :
- Utilisateur n'a aucune jauge pour les tags du contenu → score_interets = 0.5 (valeur neutre par défaut)
- Contenu avec 1 seul tag → score_interets = gauge_value / 100
- Jauges multiples → moyenne arithmétique simple (pas de pondération différente par tag)
- **Score géo excellent MAIS intérêts nuls** : Le contenu peut quand même être recommandé grâce à la pondération géographique. Exemple : contenu géo-ancré à 100m avec score_geo=1.0 et score_interets=0.0 obtient score_final = (1.0 × 0.7) + (0.0 × 0.1) + engagement = 0.7 + engagement. Ce comportement est accepté pour MVP car (1) le quota 6 contenus géolocalisés/h protège du spam, (2) l'info peut être utile contextuellement même sans intérêt marqué, (3) la distinction info/divertissement est reportée post-MVP.

**Pondérations par type** :

| Type | Poids géo | Poids intérêts |
|------|-----------|----------------|
| Géo-ancré | 0.7 | 0.1 |
| Géo-contextuel | 0.5 | 0.3 |
| Géo-neutre | 0.2 | 0.6 |

**Paramètres** :
- Distance max recommandée : **200 km**
- Dégradation : **linéaire** (1 - distance/200km)
- Rayon point GPS : **500m** (adapté au volume de contenu local)

**Tous ces paramètres sont configurables à chaud via interface admin.**

**Justification** :
- Flexibilité totale selon type de contenu
- Linéaire = rattrapage naturel du contenu viral ancien
- Auditable via métriques engagement (moyenne/médiane)

---

### 2.3 Score d'engagement et popularité

**Décision** : Intégration popularité avec poids 0.2

**Métriques** :
- **Taux de complétion** : écoutes >80% / total écoutes pertinentes (poids 0.5)
- **Ratio likes** : likes / écoutes (poids 0.3)
- **Ratio abonnements** : nouveaux abonnés après écoute / écoutes (poids 0.2)

**Distinction sources et abonnements** (neutralisation pénalités) :

Les métriques d'engagement **ne comptent que les écoutes pertinentes** pour éviter de pénaliser injustement les créateurs :

| Source écoute | Abonné au créateur ? | Skip <10s pénalise ? | Compte dans "total écoutes" ? | Justification |
|---------------|---------------------|---------------------|------------------------------|---------------|
| **`recommendation`** | ❌ Non | ✅ Oui | ✅ Oui | Skip = mauvaise recommandation OU mauvais contenu |
| **`recommendation`** | ✅ Oui | ❌ **Non** | ❌ **Non** | Abonné intéressé globalement, skip contextuel |
| **`search`** | Peu importe | ❌ Non | ❌ Non | User cherchait quelque chose de précis, skip = "pas maintenant" |
| **`direct_link`** | Peu importe | ❌ Non | ❌ Non | User curieux, peut skip sans jugement qualité |
| **`profile`** | Peu importe | ❌ Non | ❌ Non | User explore catalogue créateur |
| **`history`** | Peu importe | ❌ Non | ❌ Non | Pas une première écoute |
| **`live_notification`** | ❌ Non | ✅ Oui | ✅ Oui | Abonné normalement intéressé |
| **`live_notification`** | ✅ Oui | ❌ **Non** | ❌ **Non** | Abonné = affinité, skip contextuel |
| **`audio_guide`** | Peu importe | ❌ Non | ❌ Non | Navigation guidée, pas jugement qualité |

**Calcul engagement créateur** (exemple SQL) :

```sql
SELECT
    content_id,
    AVG(completion_rate) as avg_completion,
    COUNT(*) FILTER (WHERE completion_rate > 0.8) as complete_listens,
    COUNT(*) FILTER (WHERE completion_rate < 0.1 AND NOT is_subscribed) as penalizing_skips
FROM user_listening_history
WHERE source IN ('recommendation', 'live_notification') -- Sources pertinentes
GROUP BY content_id;
```

**Seuil minimum** :
- Minimum **50 écoutes pertinentes** avant de considérer l'engagement
- Contenu <50 écoutes : score engagement = 0.5 (neutre)

**Contenu viral** :
- Un contenu viral à Paris **peut** être proposé à Marseille
- Score géo faible compensé par score engagement élevé
- Paramétrable admin

**Dépréciation temporelle** :
- Pas de dépréciation automatique
- Ratio linéaire = contenu ancien mais toujours apprécié reste pertinent

**Justification** :
- Équilibre découverte / qualité
- **Protection créateur** : abonnés fidèles ne pénalisent pas les métriques
- **Anti-raid naturel** : skips via search/direct_link ne comptent pas (raid inefficace)
- **Cohérence UX** : abonnement = signal d'affinité fort, skip ponctuel ≠ rejet créateur
- Pas de pénalisation arbitraire des contenus anciens
- Coût : calculs sur métriques existantes + colonne `source` + colonne `is_subscribed`

---

### 2.4 Part d'aléatoire (exploration)

**Décision** : 10% par défaut, paramétrable utilisateur

**Fonctionnement** :
- 1 contenu sur 10 = tirage aléatoire (hors historique déjà écouté)
- Utilisateur peut ajuster : curseur 0% (aucun aléatoire) à 50% (exploration max)

**Curseur utilisateur** :
- 🎯 **0%** : Personnalisé max (recommandations strictes)
- ⚖️ **10%** : Équilibré (défaut)
- 🎲 **30%** : Découverte élevée
- 🌍 **50%** : Découverte max (équivaut à national = découverte)

**Justification** :
- Évite la bulle de filtre
- Laisse l'utilisateur maître de son expérience
- Coût : variable aléatoire en algo

---

### 2.5 Contenu politique (version MVP simplifiée)

> ⚠️ **Note** : La classification politique avancée (échelle gauche/droite, équilibrage imposé) a été reportée post-MVP. Voir [ANNEXE-POST-MVP.md](ANNEXE-POST-MVP.md) pour la version complète.

**Décision MVP** : Tag simple "Politique" sans classification idéologique

**Tagging** :
- Créateur peut taguer son contenu comme "Politique" (optionnel)
- Tag "Politique" au même niveau que "Économie", "Sport", "Culture", etc.
- **Pas de classification gauche/droite**
- **Pas d'équilibrage imposé**

**Filtrage utilisateur** :
- Option paramètres : **"Masquer contenu politique"**
- Si activé → 0% de contenus tagués "Politique" dans le feed
- Par défaut : désactivé (tous contenus visibles)

**Justification MVP** :
- **Simplicité** : Pas de modération politique coûteuse (~2000€/mois économisés)
- **Neutralité technique** : Aucun jugement éditorial sur orientation
- **Risque minimal** : Évite controverses et contentieux DSA au lancement
- **Fonctionnel** : Utilisateurs peuvent filtrer si souhaité

**Post-MVP** :
- Classification avancée possible si forte demande utilisateurs
- Nécessite ressources modération dédiées et audit DSA

---

### 2.6 Mode Kids (13-15 ans)

**Décision** : Mode optionnel pour adolescents 13-15 ans uniquement

> ⚠️ **Note** : Âge minimum d'inscription = **13 ans** (obligation légale EU). Pas d'utilisateurs <13 ans sur la plateforme.

**Tranche concernée** :

| Tranche | Description | Contenus autorisés | Restrictions |
|---------|-------------|-------------------|--------------|
| **13-15 ans** | Collège | Contenus "Tous publics" uniquement | Filtrage 16+ et 18+ |

**Activation** :
- ❌ **Pas d'activation automatique** (tous les utilisateurs ont ≥13 ans)
- ✅ **Activation manuelle** via toggle paramètres
- ✅ Parents peuvent activer pour leurs enfants 13-15 ans
- ✅ Utilisateur peut désactiver à tout moment

**Filtrage quand Mode Kids activé** :
- ✅ Contenus "Tous publics" uniquement
- ❌ Exclusion contenus 16+ et 18+
- ❌ Pas de contenu politique (automatiquement filtré)
- ❌ Pas de publicité (ou uniquement pub validée manuellement)

**Interface** :
- Interface standard (pas d'interface dédiée enfants pour MVP)
- Filtrage algorithmique des contenus inappropriés

**Justification** :
- **Conformité légale** : Âge minimum 13 ans (RGPD, DSA)
- **Simplicité MVP** : Un seul mode optionnel vs 4 tranches d'âge
- **Protection mineurs** : Filtrage contenus adultes pour 13-15 ans
- **Flexibilité** : Parents décident d'activer ou non

---

### 2.7 Déclenchement géographique

**Décision** : Notification au passage, pas d'anticipation

**Fonctionnement** :
1. Utilisateur passe à <500m d'un point GPS (contenu géo-ancré)
2. **Notification sonore** (bip court) + **visuelle** (logo selon type)
3. Types de logos : 📍 Info, 🏛️ Culturel, 🍴 Commercial, 🎭 Événement
4. Délai réaction utilisateur : **5 secondes** pour accepter (bouton volant ou commande vocale)
5. Si accepté → lecture immédiate
6. Si ignoré → contenu proposé normalement en file d'attente

**Publicités** :
- ⚠️ **Jamais d'interruption** de contenu en cours
- Pub s'intercale **entre deux séquences** uniquement
- Notification pub : son différent (facultatif selon paramètres)

**Gestion demi-tour** :
- Si utilisateur repart du point après notification → pas de nouvelle notification (déjà proposé)
- Réinitialisation après 24h

**Justification** :
- Respect écoute en cours (pas de coupure brutale)
- UX fluide (utilisateur garde contrôle)
- Simplicité technique (pas de prédiction trajectoire)

---

### 2.8 Historique et repropositon

**Décision** : Pas de reproposition sauf contenu partiel ou skip d'abonné

**Règles** :

| État écoute | Completion | Abonné au créateur ? | Action |
|-------------|------------|---------------------|--------|
| **Écouté complètement** | >80% | Peu importe | ❌ Ne jamais reproposer (sauf flag `replayable = true` pour audio-guides) |
| **Skippé rapidement** | <10s | ❌ Non | ❌ Ne pas reproposer (signal négatif clair) |
| **Skippé rapidement** | <10s | ✅ **Oui** | ✅ **Peut reproposer** (abonnement = affinité, skip contextuel) |
| **Partiellement écouté** | 10-80% | Peu importe | ✅ Reproposer avec reprise position (`last_position_seconds`) |

**Stockage historique** :
- Table `user_content_history` (user_id, content_id, creator_id, **is_subscribed**, completion_rate, last_position, listened_at)
- Historique **illimité** (PostgreSQL)
- Algorithme considère les **100 derniers** pour optimisation requêtes
- Export complet disponible (RGPD)

**Colonne `is_subscribed`** :
- Booléen stockant si l'utilisateur était abonné au créateur **au moment de l'écoute**
- Permet de distinguer les skips d'abonnés (contextuels) des skips de non-abonnés (désintérêt)
- Utilisé pour décisions de reproposition et calculs d'engagement

**Justification** :
- Découverte maximale (pas de redites)
- **Cohérence abonnement** : un skip ponctuel d'un abonné ≠ rejet du créateur (peut être contextuel : "pas maintenant", "pas ce sujet", "mauvais timing")
- Respect erreurs de clic (contenu partiel = 2nde chance)
- Coût stockage négligeable (PostgreSQL scalable)

---

### 2.9 Paramétrabilité admin (interface dashboard)

**Décision** : Tous paramètres scoring exposés + A/B testing

**Paramètres configurables à chaud** :

| Paramètre | Plage | Défaut | Unité |
|-----------|-------|--------|-------|
| `poids_geo_ancre` | 0.5 - 1.0 | 0.7 | % |
| `poids_geo_contextuel` | 0.3 - 0.7 | 0.5 | % |
| `poids_geo_neutre` | 0.0 - 0.4 | 0.2 | % |
| `poids_engagement` | 0.0 - 0.5 | 0.2 | % |
| `part_aleatoire_global` | 0.0 - 0.3 | 0.1 | % |
| `distance_max_km` | 50 - 500 | 200 | km |
| `rayon_gps_point_m` | 100 - 2000 | 500 | m |
| `seuil_min_ecoutes_engagement` | 10 - 200 | 50 | nb |

**Application changements** :
- Immédiat : nouveaux calculs utilisent nouvelle config
- Aucun recalcul batch (coût CPU)
- Version config trackée (git-like)
- Rollback 1 clic

**A/B Testing** :
- Création variantes (Config A vs Config B)
- Split utilisateurs 50/50 aléatoire
- Métriques comparatives : taux complétion, engagement, session duration
- Dashboard graphique temps réel

**Audit engagement** :
- Métriques clés : moyenne/médiane temps d'écoute par session
- Graphiques : évolution engagement selon config
- Export CSV pour analyse externe

**Justification** :
- Optimisation continue sans redéploiement
- Data-driven decisions (métriques objectives)
- Coût : dashboard admin à développer (one-time)

---

### 2.10 Paramétrabilité utilisateur

**Décision** : Curseurs avancés avec profils sauvegardables

**Niveaux de personnalisation** :

**Curseurs disponibles** :
- 📍 **Géolocalisation** : Local ← slider → National (découverte = national)
- 🎲 **Découverte** : 0% ← slider → 50% (part aléatoire)
- ⚖️ **Politique** : Masquer / Équilibré / Mes préférences

**Profils sauvegardables** :
- 🚗 Trajet quotidien (boulot) : géo local, découverte 5%, politique masqué
- 🛣️ Road trip : géo régional, découverte 30%, politique équilibré
- 👶 Enfants : Mode Kids activé

**Synchronisation** :
- ✅ Sync profils entre devices (cloud PostgreSQL)
- ❌ Pas de partage profils entre utilisateurs (famille)
- Auto-switch selon context (détection trajet récurrent via GPS)

**Sécurité conduite** :
- ⚠️ **Blocage modification si vitesse GPS >10 km/h**
- Warning au lancement app : "Configurez avant de prendre la route"
- Modifications uniquement app arrêtée/passager

**Justification** :
- Utilisateur maître de son expérience
- Contextes d'usage différents (quotidien vs voyage)
- Sécurité routière (pas de distraction)

---

### 2.11 Médias traditionnels

**Décision** : Ouverture aux médias établis

**Médias autorisés** :
- Presse nationale : Le Monde, Le Parisien, Libération, Le Figaro, etc.
- Radios : France Inter, RTL, Europe 1, etc.
- Médias régionaux : Ouest-France, Sud-Ouest, etc.

**Format contenus** :
- Flashs info géolocalisés (actualité régionale)
- Chroniques thématiques (culture, économie, sport)
- Éditos et débats (classification politique appliquée)

**Validation** :
- Compte média vérifié (badge ✓)
- Pas de validation 3 premiers contenus (confiance établie)
- Modération a posteriori uniquement

**Monétisation** :
- Partage revenus pub standard (même conditions créateurs)
- Possibilité sponsoring direct (pas via plateforme)

**Justification** :
- Crédibilité plateforme (contenus professionnels)
- Diversité éditoriale
- Attractivité grand public (noms reconnus)

---

## Récapitulatif Section 2
