## 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 [../../_shared/rules/ANNEXE-POST-MVP.md](../../_shared/rules/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