feat: initial commit
46 changed files +16,247 −0
added
.env.example
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: .env.example | |
| 4 | +# Desc: Variables d'environnement (copier vers .env — JAMAIS commité, §16) | |
| 5 | +# ============================================================================== | |
| 6 | +# Scrapfly — profils Instagram/TikTok (palier 4, à minimiser §10) | |
| 7 | +SCRAPFLY_KEY= | |
| 8 | +# Firecrawl — repli pages link-in-bio / listes publiques | |
| 9 | +FIRECRAWL_API_KEY= | |
| 10 | +# API officielles (privilégiées §10) | |
| 11 | +YOUTUBE_API_KEY= | |
| 12 | +TWITCH_CLIENT_ID= | |
| 13 | +TWITCH_CLIENT_SECRET= | |
added
.gitignore
+12 −0
@@ -0,0 +1,12 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: .gitignore | |
| 4 | +# Desc: Exclusions git — secrets, base de données, caches | |
| 5 | +# ============================================================================== | |
| 6 | +.env | |
| 7 | +__pycache__/ | |
| 8 | +*.pyc | |
| 9 | +data/creaka.db* | |
| 10 | +data/optout_requests.jsonl | |
| 11 | +.pytest_cache/ | |
| 12 | +.venv/ | |
added
CLAUDE.md
+519 −0
@@ -0,0 +1,519 @@ | ||
| 1 | +<!-- | |
| 2 | +================================================================================ | |
| 3 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 4 | + File: CLAUDE.md | |
| 5 | + Project: Créa·Ka — Agrégateur des créateurs de contenu & influenceurs québécois | |
| 6 | +================================================================================ | |
| 7 | +--> | |
| 8 | + | |
| 9 | +# CLAUDE.md — Créa·Ka | |
| 10 | + | |
| 11 | +> **Agrégateur exhaustif des créateurs de contenu et influenceurs québécois, | |
| 12 | +> avec liens vers TOUTES leurs plateformes.** | |
| 13 | +> Chaque créateur, tous ses comptes (Instagram, TikTok, YouTube, Twitch, X, | |
| 14 | +> Facebook, Snapchat, Kick, Substack, Patreon…), regroupés, cherchables et | |
| 15 | +> découvrables par niche, région, langue et taille d'audience. | |
| 16 | + | |
| 17 | +Ce fichier est la source de vérité pour tout agent Claude (ou humain) qui travaille | |
| 18 | +sur ce dépôt. Lis-le **en entier** avant d'écrire une seule ligne de code. | |
| 19 | + | |
| 20 | +--- | |
| 21 | + | |
| 22 | +## 0. À LIRE EN PREMIER — Règles non négociables | |
| 23 | + | |
| 24 | +1. **En-tête d'auteur obligatoire.** **Chaque fichier** du dépôt (code, config, | |
| 25 | + doc, script) doit commencer par un en-tête mentionnant l'auteur : | |
| 26 | + `Simon-Pierre Boucher <contact@spboucher.ai>`. Voir la **section 17** pour le | |
| 27 | + format exact. Aucun fichier ne doit être commité sans cet en-tête. | |
| 28 | + | |
| 29 | +2. **Inspire-toi du dépôt Lou·Ka.** Avant de concevoir ou d'écrire **le moindre | |
| 30 | + connecteur**, va lire le **dépôt de Lou·Ka** (le site jumeau, agrégateur de | |
| 31 | + logements). Ses connecteurs sont le patron de référence : structure des modules, | |
| 32 | + extraction (Firecrawl / Scrapfly), normalisation, déduplication, gestion des | |
| 33 | + mises à jour, planification. **Ne réinvente rien** : reprends les mêmes | |
| 34 | + conventions, la même architecture, les mêmes utilitaires partagés. Créa·Ka doit | |
| 35 | + être cohérent avec l'écosystème `·Ka`. Voir **section 3**. | |
| 36 | + | |
| 37 | +3. **Travail de moine.** L'agrégation est minutieuse, créateur par créateur, | |
| 38 | + plateforme par plateforme, sans rien précipiter. Un mauvais rattachement de | |
| 39 | + compte (attribuer à un créateur un profil qui n'est pas le sien) est une faute | |
| 40 | + grave. Mieux vaut un lien vérifié de moins qu'un lien erroné. | |
| 41 | + | |
| 42 | +4. **Vérifie l'existant avant d'ajouter.** Avant d'intégrer un créateur ou une | |
| 43 | + source, confirme qu'il/elle n'est **pas déjà couvert(e)** (index créateurs, | |
| 44 | + registre des sources — sections 9 et 12). Un créateur = **une fiche canonique**, | |
| 45 | + quelles que soient les plateformes et sources qui l'alimentent. | |
| 46 | + | |
| 47 | +5. **Chaque plateforme a son connecteur sur mesure**, mais les sources qui | |
| 48 | + partagent une même mécanique (API d'une plateforme, agrégateurs de liens type | |
| 49 | + Linktree) sont couvertes par un **connecteur générique templatisé** réutilisable. | |
| 50 | + Voir **section 8**. | |
| 51 | + | |
| 52 | +6. **VIE PRIVÉE & ÉTHIQUE — règle sacrée.** Créa·Ka n'agrège **que des profils | |
| 53 | + publics et professionnels** de créateurs qui se présentent publiquement comme | |
| 54 | + tels. **Jamais** de données privées, de coordonnées personnelles non publiées, | |
| 55 | + ni de compilation intrusive. Mécanisme d'**opt-out** obligatoire. Prudence | |
| 56 | + extrême avec les **créateurs mineurs**. Voir **section 15** — cette section prime | |
| 57 | + sur toute autre considération de complétude ou de volume. | |
| 58 | + | |
| 59 | +--- | |
| 60 | + | |
| 61 | +## 1. Mission | |
| 62 | + | |
| 63 | +Rendre visible, en un seul endroit, **l'ensemble des créateurs de contenu et | |
| 64 | +influenceurs québécois**, avec **un lien direct vers chacune de leurs plateformes**, | |
| 65 | +et permettre de les **découvrir** par niche, région, langue et taille d'audience. | |
| 66 | + | |
| 67 | +En clair : un croisement entre un **annuaire** (recherche et découverte) et un | |
| 68 | +**« Linktree collectif »** (tous les comptes d'un créateur au même endroit), | |
| 69 | +centré sur le Québec. | |
| 70 | + | |
| 71 | +### Ce qui nous distingue | |
| 72 | + | |
| 73 | +Les bases d'influenceurs existantes (Heepsy, Upfluence, HypeAuditor) sont | |
| 74 | +**payantes, généralistes et anglophones**, pensées pour les marques. Les « tops | |
| 75 | +influenceurs » des médias québécois sont **ponctuels et non exhaustifs**. Personne | |
| 76 | +n'offre un **annuaire public, québécois, exhaustif et cross-plateforme**. C'est le | |
| 77 | +créneau de Créa·Ka. | |
| 78 | + | |
| 79 | +--- | |
| 80 | + | |
| 81 | +## 2. Périmètre — TOUS les créateurs québécois | |
| 82 | + | |
| 83 | +Périmètre maximaliste. Si la personne crée du contenu publiquement et a une audience | |
| 84 | +au Québec, elle a sa place : | |
| 85 | + | |
| 86 | +- **Influenceurs** (Instagram, TikTok, Snapchat) : mode, beauté, lifestyle, famille. | |
| 87 | +- **YouTubeurs** : vlog, humour, gaming, tech, éducation, cuisine. | |
| 88 | +- **Streamers** : Twitch, Kick, YouTube Live (gaming, IRL, just chatting). | |
| 89 | +- **Créateurs TikTok** toutes niches. | |
| 90 | +- **Podcasteurs** et créateurs audio. | |
| 91 | +- **Créateurs écrits** : Substack, blogues, infolettres. | |
| 92 | +- **Créateurs sur abonnement** : Patreon (contenu grand public/professionnel). | |
| 93 | +- **Créateurs de niche** : sport, plein air, finance, humour, musique, arts, | |
| 94 | + cuisine, parentalité, tech, jeux vidéo, etc. | |
| 95 | +- **Micro et nano-créateurs** locaux (souvent ignorés des bases existantes). | |
| 96 | + | |
| 97 | +> Règle d'or : en cas de doute sur l'inclusion, **on inclut** et on catégorise — | |
| 98 | +> **sauf** si la règle de vie privée/éthique (section 15) s'y oppose. | |
| 99 | + | |
| 100 | +--- | |
| 101 | + | |
| 102 | +## 3. Dépôts de référence — étudier Lou·Ka AVANT de coder | |
| 103 | + | |
| 104 | +Créa·Ka fait partie de la famille `·Ka`. **Le dépôt de Lou·Ka est la référence | |
| 105 | +canonique pour l'architecture des connecteurs.** Avant tout développement : | |
| 106 | + | |
| 107 | +1. **Ouvrir le dépôt de Lou·Ka** et localiser le dossier des connecteurs | |
| 108 | + (typiquement `connectors/`, `scrapers/` ou `sources/`). | |
| 109 | +2. **Lire 2 ou 3 connecteurs complets** pour comprendre : structure d'un module, | |
| 110 | + extraction (Firecrawl / Scrapfly, endpoints JSON), normalisation, déduplication, | |
| 111 | + mises à jour, planification (cadence, retries, backoff), erreurs, logging. | |
| 112 | +3. **Réutiliser** les utilitaires partagés de Lou·Ka (clients Firecrawl/Scrapfly, | |
| 113 | + helpers de normalisation, déduplication, géocodage) plutôt que d'en réécrire. | |
| 114 | +4. **Calquer** conventions de nommage, structure des dossiers et style de code. | |
| 115 | + | |
| 116 | +> Adaptation propre à Créa·Ka : là où Lou·Ka normalise une **annonce**, Créa·Ka | |
| 117 | +> normalise un **créateur + N comptes de plateformes rattachés**. Le défi central | |
| 118 | +> n'est pas le volume par source mais la **résolution d'identité** : rattacher les | |
| 119 | +> bons comptes à la bonne personne (section 12). | |
| 120 | + | |
| 121 | +> Si tu ne trouves pas le dépôt Lou·Ka ou l'accès aux connecteurs, **arrête-toi et | |
| 122 | +> demande** plutôt que d'inventer une architecture divergente. | |
| 123 | + | |
| 124 | +Écosystème complet, pour contexte : | |
| 125 | + | |
| 126 | +| Site | Domaine | Agrège | | |
| 127 | +|--------------|------------------|----------------------------------------------| | |
| 128 | +| Lou·Ka | lou-ka.com | Logements à louer | | |
| 129 | +| Immo·Ka | immo-ka.com | Propriétés à vendre | | |
| 130 | +| Auto·Ka | auto-ka.com | Véhicules usagés | | |
| 131 | +| Food·Ka | food-ka.com | Prix d'épicerie & circulaires | | |
| 132 | +| Fabri·Ka | fabri-ka.com | Produits québécois | | |
| 133 | +| Sorti·Ka | sorti-ka.com | Sorties & événements | | |
| 134 | +| Resto·Ka | resto-ka.com | Restaurants, menus & prix | | |
| 135 | +| **Créa·Ka** | **crea-ka.com** | **Créateurs & influenceurs (ce dépôt)** | | |
| 136 | + | |
| 137 | +--- | |
| 138 | + | |
| 139 | +## 4. Architecture | |
| 140 | + | |
| 141 | +Pipeline `·Ka`, adapté aux créateurs (l'étape clé en plus : **résolution d'identité**) : | |
| 142 | + | |
| 143 | +``` | |
| 144 | + ┌────────────┐ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ ┌────────┐ | |
| 145 | + │ DÉCOUVERTE │──▶│ EXTRACTION │──▶│ RÉSOLUTION │──▶│ NORMALISATION│──▶│ DÉDUP. │──▶│STOCKAGE│ | |
| 146 | + │ (trouver │ │ FC/Scrapfly│ │ D'IDENTITÉ │ │ (schéma │ │ (créateur│ │ (DB) │ | |
| 147 | + │ créateurs)│ │ + APIs │ │ (lier comptes)│ │ commun) │ │ unique) │ │ │ | |
| 148 | + └────────────┘ └────────────┘ └──────────────┘ └──────────────┘ └──────────┘ └───┬────┘ | |
| 149 | + │ | |
| 150 | + ┌──────────▼──────────┐ | |
| 151 | + │ API / RECHERCHE / UI │ | |
| 152 | + │ (annuaire + liens) │ | |
| 153 | + └─────────────────────┘ | |
| 154 | +``` | |
| 155 | + | |
| 156 | +- **Découverte** : trouver les créateurs québécois (hashtags, géo, listes, agences). | |
| 157 | +- **Extraction** : enrichir chaque profil (bio, abonnés, liens) via API ou scraping. | |
| 158 | +- **Résolution d'identité** : rattacher tous les comptes d'une même personne | |
| 159 | + (souvent via sa page de liens type Linktree). | |
| 160 | +- **Normalisation / Déduplication / Stockage** : une fiche créateur canonique. | |
| 161 | +- **API / UI** : recherche et découverte par niche, région, langue, taille. | |
| 162 | + | |
| 163 | +--- | |
| 164 | + | |
| 165 | +## 5. Modèle de données — schéma Créateur + Plateformes | |
| 166 | + | |
| 167 | +```jsonc | |
| 168 | +{ | |
| 169 | + "id": "string", // * id interne canonique | |
| 170 | + "source_ids": ["linktree:handle"], // * id(s) d'origine par source | |
| 171 | + "display_name": "string", // * nom public / nom de scène | |
| 172 | + "legal_name": "string|null", // seulement si rendu public par le créateur | |
| 173 | + "bio": "string", // bio publique agrégée / résumée | |
| 174 | + "niches": ["humour", "gaming"], // * taxonomie (section 6) | |
| 175 | + "creator_type": "streamer", // influenceur|youtubeur|streamer|podcasteur|... | |
| 176 | + "region": "string", // une des 17 régions si connu (section 7) | |
| 177 | + "city": "string|null", | |
| 178 | + "languages": ["fr"], // fr | en | bilingue | |
| 179 | + "primary_platform": "youtube", // * plateforme principale | |
| 180 | + "platforms": [ // * TOUS les comptes rattachés | |
| 181 | + { | |
| 182 | + "platform": "youtube", // * (section 6.2) | |
| 183 | + "handle": "@nom", // * | |
| 184 | + "url": "https://youtube.com/@nom",// * | |
| 185 | + "followers": 125000, // abonnés (peut être null) | |
| 186 | + "verified": true, | |
| 187 | + "confidence": 0.98, // * confiance du rattachement (section 12) | |
| 188 | + "last_checked": "ISO-8601" // * | |
| 189 | + } | |
| 190 | + ], | |
| 191 | + "link_in_bio_url": "https://linktr.ee/nom", // page de liens source, si trouvée | |
| 192 | + "total_reach": 350000, // somme dédupliquée des audiences (indicatif) | |
| 193 | + "audience_tier": "macro", // section 6.3 | |
| 194 | + "business_contact": "string|null", // courriel pro SEULEMENT si public (section 15) | |
| 195 | + "agency": "string|null", // agence / management si public | |
| 196 | + "is_minor": false, // * drapeau si le créateur est mineur (§15) | |
| 197 | + "status": "active", // active | inactive | opted_out | |
| 198 | + "first_seen": "ISO-8601", // * | |
| 199 | + "last_seen": "ISO-8601", // * | |
| 200 | + "updated_at": "ISO-8601" // * | |
| 201 | +} | |
| 202 | +``` | |
| 203 | + | |
| 204 | +Règles : | |
| 205 | +- `platforms` est le cœur : **tous** les comptes publics, chacun avec son `url` et | |
| 206 | + son `confidence` de rattachement. | |
| 207 | +- Ne jamais inscrire `legal_name`, `business_contact` ou toute coordonnée **non | |
| 208 | + rendue publique** par le créateur lui-même (section 15). | |
| 209 | +- `is_minor` : si détecté, appliquer le régime restreint de la section 15. | |
| 210 | + | |
| 211 | +--- | |
| 212 | + | |
| 213 | +## 6. Taxonomies | |
| 214 | + | |
| 215 | +### 6.1 Niches | |
| 216 | +`humour` · `mode` · `beaute` · `lifestyle` · `famille-parentalite` · `cuisine` · | |
| 217 | +`gaming` · `tech` · `sport-fitness` · `plein-air` · `voyage` · `musique` · `arts` · | |
| 218 | +`danse` · `education` · `finance-affaires` · `sante-mieux-etre` · `bouffe-resto` · | |
| 219 | +`actualite-opinion` · `autre` | |
| 220 | + | |
| 221 | +### 6.2 Plateformes (`platform`) | |
| 222 | +`instagram` · `tiktok` · `youtube` · `twitch` · `kick` · `x` · `facebook` · | |
| 223 | +`snapchat` · `substack` · `patreon` · `linkedin` · `threads` · `podcast` · | |
| 224 | +`site-web` · `autre` | |
| 225 | + | |
| 226 | +### 6.3 Taille d'audience (`audience_tier`) — basée sur l'audience principale | |
| 227 | +- `nano` (< 10k) · `micro` (10k–100k) · `macro` (100k–1M) · `mega` (> 1M). | |
| 228 | + | |
| 229 | +### 6.4 Langue | |
| 230 | +`fr` · `en` · `bilingue` | |
| 231 | + | |
| 232 | +--- | |
| 233 | + | |
| 234 | +## 7. Régions du Québec (quand connu) | |
| 235 | + | |
| 236 | +Bas-Saint-Laurent · Saguenay–Lac-Saint-Jean · Capitale-Nationale · Mauricie · | |
| 237 | +Estrie · Montréal · Outaouais · Abitibi-Témiscamingue · Côte-Nord · | |
| 238 | +Nord-du-Québec · Gaspésie–Îles-de-la-Madeleine · Chaudière-Appalaches · Laval · | |
| 239 | +Lanaudière · Laurentides · Montérégie · Centre-du-Québec. | |
| 240 | + | |
| 241 | +La région est souvent **inconnue ou non déclarée** pour un créateur — c'est | |
| 242 | +acceptable (`region: null`). Ne **jamais** deviner ou inférer une localisation | |
| 243 | +précise à partir de données privées. Se limiter à ce que le créateur déclare | |
| 244 | +publiquement. | |
| 245 | + | |
| 246 | +--- | |
| 247 | + | |
| 248 | +## 8. Philosophie des connecteurs | |
| 249 | + | |
| 250 | +- **Deux familles de connecteurs :** | |
| 251 | + 1. **Découverte** — trouver de nouveaux créateurs québécois (hashtags, géo, | |
| 252 | + listes médias, agences, palmarès). | |
| 253 | + 2. **Enrichissement** — pour un handle donné, récupérer profil, abonnés, liens. | |
| 254 | +- **Le levier nº 1 : les pages de liens (link-in-bio).** Linktree, Beacons, | |
| 255 | + Linkin.bio, etc. exposent **d'un coup tous les comptes** d'un créateur → c'est la | |
| 256 | + source la plus efficace pour remplir `platforms`. Un connecteur générique | |
| 257 | + « link-in-bio » (templatisé par service) est prioritaire. | |
| 258 | +- **Connecteurs templatisés par plateforme** pour l'enrichissement (un connecteur | |
| 259 | + YouTube, un Twitch, etc.), à réutiliser pour tous les créateurs. | |
| 260 | +- Interface commune (à calquer sur Lou·Ka) : `fetch()` → `normalize()` → `emit()`. | |
| 261 | + Idempotent, incrémental quand c'est possible. | |
| 262 | +- **Robustesse** : timeouts, retries + backoff, tolérance aux changements, alerte si | |
| 263 | + un connecteur cesse de retourner des données (probable blocage). | |
| 264 | + | |
| 265 | +--- | |
| 266 | + | |
| 267 | +## 9. Catalogue des sources à connecter (priorisé) | |
| 268 | + | |
| 269 | +> ⚠️ Vérifie l'index des créateurs et le registre des sources **avant** d'ajouter. | |
| 270 | + | |
| 271 | +### Palier 1 — Pages de liens (link-in-bio) : révèlent toutes les plateformes | |
| 272 | +- **Linktree**, **Beacons**, **Linkin.bio (Later)**, **Milkshake**, **Campsite**, | |
| 273 | + **Snipfeed**. Une seule page → tous les comptes du créateur. À templatiser. | |
| 274 | + | |
| 275 | +### Palier 2 — API officielles des plateformes (enrichissement fiable) | |
| 276 | +- **YouTube Data API**, **Twitch API**, **X/Threads API** (selon accès), **TikTok** | |
| 277 | + et **Instagram** (via API officielles/Graph quand disponibles). Respecter quotas | |
| 278 | + et CGU. À privilégier sur le scraping. | |
| 279 | + | |
| 280 | +### Palier 3 — Découverte de créateurs québécois | |
| 281 | +- **Hashtags & mots-clés** : #quebec, #mtl, #madeinquebec, #créateurquébécois, etc. | |
| 282 | +- **Signaux de langue (français) et de lieu** déclarés publiquement. | |
| 283 | +- **Listes et palmarès médias** (« influenceurs québécois »), articles de presse. | |
| 284 | +- **Palmarès / galas** du milieu (récompenses de création numérique). | |
| 285 | +- **Agences de talents / management** québécoises (rosters publics). | |
| 286 | + | |
| 287 | +### Palier 4 — Profils publics des plateformes (scraping encadré) | |
| 288 | +- Pages de profil publiques (bio, liens, abonnés) quand l'API ne suffit pas — | |
| 289 | + **Scrapfly** requis, **CGU à respecter**, voir section 15. | |
| 290 | + | |
| 291 | +### Palier 5 — Créateurs sur abonnement / écrits | |
| 292 | +- **Substack**, **Patreon** (pages publiques : nom, thème, liens ; **pas** le | |
| 293 | + contenu réservé ni les données d'abonnés). | |
| 294 | + | |
| 295 | +--- | |
| 296 | + | |
| 297 | +## 10. Extraction — Firecrawl, Scrapfly & APIs | |
| 298 | + | |
| 299 | +- **API officielle de la plateforme** → toujours privilégié (YouTube, Twitch, etc.). | |
| 300 | + Fiable, structuré, conforme. | |
| 301 | +- **Parsing des pages link-in-bio** (Firecrawl) → extraire la liste des liens/comptes. | |
| 302 | +- **Firecrawl** → pages publiques relativement ouvertes (agences, listes, blogues). | |
| 303 | +- **Scrapfly** → plateformes hostiles au scraping (Instagram, TikTok) : anti-bot, | |
| 304 | + rendu JS, rotation d'IP. À **minimiser** et n'utiliser que faute d'API, dans le | |
| 305 | + respect des CGU (section 15). | |
| 306 | + | |
| 307 | +Chaque connecteur **documente** en tête son mode d'accès (API / Firecrawl / Scrapfly) | |
| 308 | +et pourquoi. Réutiliser les clients partagés de Lou·Ka. | |
| 309 | + | |
| 310 | +--- | |
| 311 | + | |
| 312 | +## 11. Normalisation | |
| 313 | + | |
| 314 | +- Nettoyer noms, bios, handles (emoji superflus, doublons, casse). | |
| 315 | +- Mapper vers le **schéma Créateur + Plateformes** (section 5). | |
| 316 | +- Classer **niches**, **type de créateur**, **langue**, **taille d'audience**. | |
| 317 | +- Normaliser chaque `url` de plateforme (forme canonique par plateforme). | |
| 318 | +- Rattacher une **région** uniquement si déclarée publiquement (sinon `null`). | |
| 319 | +- Renseigner `confidence` de chaque rattachement de compte (section 12). | |
| 320 | +- Conserver les **`source_ids`** préfixés (ex. `linktree:nom`, `youtube:UCxxxx`). | |
| 321 | + | |
| 322 | +--- | |
| 323 | + | |
| 324 | +## 12. Résolution d'identité & déduplication — LE cœur du projet | |
| 325 | + | |
| 326 | +Le vrai défi n'est pas le volume, c'est de **rattacher les bons comptes à la bonne | |
| 327 | +personne** et de **ne pas fusionner deux personnes distinctes**. | |
| 328 | + | |
| 329 | +### 12.1 Rattachement des comptes (identity resolution) | |
| 330 | +Signaux, du plus fort au plus faible : | |
| 331 | +1. **Page link-in-bio** listant explicitement les comptes → rattachement **fort**. | |
| 332 | +2. **Liens croisés** : un profil qui pointe vers un autre (IG → TikTok en bio). | |
| 333 | +3. **Handle identique** + même nom d'affichage + même photo sur plusieurs plateformes. | |
| 334 | +4. **Mentions réciproques / mêmes liens externes**. | |
| 335 | +- Chaque rattachement reçoit un score `confidence`. En dessous d'un seuil, marquer | |
| 336 | + « à vérifier » plutôt que de rattacher à tort. | |
| 337 | + | |
| 338 | +### 12.2 Déduplication des créateurs | |
| 339 | +- **Clé** : ensemble de comptes rattachés + nom d'affichage normalisé. | |
| 340 | +- Deux fiches qui partagent au moins un compte à `confidence` élevée → fusion. | |
| 341 | +- **Homonymes** : ne **jamais** fusionner sur le seul nom. Exiger un compte partagé. | |
| 342 | +- À la fusion : union des `platforms`, meilleure source par champ, on **ne perd | |
| 343 | + aucun** lien vérifié. | |
| 344 | + | |
| 345 | +S'inspirer de la déduplication de **Lou·Ka** (entités multi-sources), en gardant à | |
| 346 | +l'esprit qu'ici une erreur de fusion mélange deux **personnes réelles** — donc | |
| 347 | +**prudence maximale**. | |
| 348 | + | |
| 349 | +--- | |
| 350 | + | |
| 351 | +## 13. Fraîcheur & états | |
| 352 | + | |
| 353 | +- **Abonnés / audience** changent constamment → rafraîchir et horodater | |
| 354 | + (`last_checked` par plateforme). | |
| 355 | +- **Comptes disparus / renommés** : marquer inactifs, conserver l'historique ; ne | |
| 356 | + pas supprimer brutalement (politique de grâce via `last_seen`, comme Lou·Ka). | |
| 357 | +- **Créateur inactif** (plus de publication) → `status: inactive`. | |
| 358 | +- **Opt-out** : si un créateur demande le retrait → `status: opted_out`, fiche | |
| 359 | + masquée et **non ré-agrégée** (voir section 15). | |
| 360 | + | |
| 361 | +--- | |
| 362 | + | |
| 363 | +## 14. Cadence de rafraîchissement | |
| 364 | + | |
| 365 | +- Métriques d'audience des gros créateurs : **hebdomadaire**. | |
| 366 | +- Enrichissement standard : **mensuel**. | |
| 367 | +- Découverte de nouveaux créateurs : **continu / hebdomadaire** selon les sources. | |
| 368 | +- Toujours mettre à jour `last_checked` et `last_seen` à chaque passage réussi. | |
| 369 | + | |
| 370 | +--- | |
| 371 | + | |
| 372 | +## 15. Vie privée, éthique & conformité — PRIME SUR TOUT | |
| 373 | + | |
| 374 | +> Créa·Ka agrège des **personnes**. Cette section a **priorité absolue** sur la | |
| 375 | +> complétude et le volume. En cas de doute, **on n'inclut pas** et on demande. | |
| 376 | + | |
| 377 | +- **Uniquement du public et du professionnel.** N'agréger que les profils publics de | |
| 378 | + créateurs qui se présentent publiquement comme créateurs/influenceurs, et **que** | |
| 379 | + les informations qu'ils rendent publiques (nom de scène, comptes, bio, liens, | |
| 380 | + courriel *pro* affiché). **Jamais** d'adresse personnelle, de téléphone privé, de | |
| 381 | + courriel personnel, ni de données déduites/privées. | |
| 382 | +- **Pas de compilation intrusive.** Ne pas croiser des sources pour reconstituer des | |
| 383 | + informations qu'une personne n'a pas choisi de rendre publiques. | |
| 384 | +- **Opt-out obligatoire et respecté.** Fournir un mécanisme simple de retrait ; | |
| 385 | + une fois `opted_out`, la fiche est masquée et **exclue des ré-agrégations futures** | |
| 386 | + (liste de suppression persistante). | |
| 387 | +- **Créateurs mineurs — prudence extrême.** Si `is_minor` est détecté (ou probable), | |
| 388 | + appliquer un régime restreint : pas de données de contact, pas de métriques | |
| 389 | + précises de localisation, et en cas de doute **exclure**. Ne jamais produire quoi | |
| 390 | + que ce soit qui pourrait faciliter le ciblage ou l'identification d'un mineur. | |
| 391 | +- **CGU & robots.txt** de chaque plateforme respectés. Privilégier les **API | |
| 392 | + officielles** ; minimiser le scraping ; rate-limiting raisonnable ; identification | |
| 393 | + honnête. Les plateformes (Instagram, TikTok) sont restrictives : ne pas | |
| 394 | + industrialiser un accès contraire à leurs conditions — **demander en cas de doute**. | |
| 395 | +- **Attribution & exactitude.** Lien vers la source ; ne republier que le nécessaire | |
| 396 | + à la découverte ; corriger vite toute erreur de rattachement (une fausse | |
| 397 | + association est un préjudice réel pour la personne). | |
| 398 | +- **Loi 25 (protection des renseignements personnels au Québec).** Concevoir en | |
| 399 | + tenant compte des obligations applicables aux renseignements personnels ; documenter | |
| 400 | + les finalités, permettre l'accès/rectification/retrait. | |
| 401 | + | |
| 402 | +--- | |
| 403 | + | |
| 404 | +## 16. Conventions de code | |
| 405 | + | |
| 406 | +- Suivre **les mêmes conventions que Lou·Ka** (langage, dossiers, nommage, linting, | |
| 407 | + formatage). Cohérence > préférences personnelles. | |
| 408 | +- Un connecteur = un module isolé, testable indépendamment. | |
| 409 | +- Secrets (clés API des plateformes, Firecrawl/Scrapfly) via variables | |
| 410 | + d'environnement / gestionnaire de secrets. Jamais en clair. | |
| 411 | +- Logging structuré (source, nb de créateurs, nb de comptes rattachés, confiance | |
| 412 | + moyenne, erreurs, durée). | |
| 413 | +- Commits atomiques, messages clairs. | |
| 414 | + | |
| 415 | +--- | |
| 416 | + | |
| 417 | +## 17. En-tête d'auteur — OBLIGATOIRE dans chaque fichier | |
| 418 | + | |
| 419 | +**Tout fichier** du dépôt doit débuter par un en-tête d'auteur. L'auteur est | |
| 420 | +toujours **Simon-Pierre Boucher \<contact@spboucher.ai\>**. | |
| 421 | + | |
| 422 | +**Python / Shell / YAML / TOML / Dockerfile :** | |
| 423 | +```python | |
| 424 | +# ============================================================================== | |
| 425 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 426 | +# File: <nom_du_fichier> | |
| 427 | +# Desc: <brève description> | |
| 428 | +# ============================================================================== | |
| 429 | +``` | |
| 430 | + | |
| 431 | +**JavaScript / TypeScript / Go / Rust / Java / C :** | |
| 432 | +```javascript | |
| 433 | +// ============================================================================== | |
| 434 | +// Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 435 | +// File: <nom_du_fichier> | |
| 436 | +// Desc: <brève description> | |
| 437 | +// ============================================================================== | |
| 438 | +``` | |
| 439 | + | |
| 440 | +**Markdown / HTML / XML :** | |
| 441 | +```html | |
| 442 | +<!-- | |
| 443 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 444 | + File: <nom_du_fichier> | |
| 445 | + Desc: <brève description> | |
| 446 | +--> | |
| 447 | +``` | |
| 448 | + | |
| 449 | +**CSS / SCSS :** | |
| 450 | +```css | |
| 451 | +/* | |
| 452 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 453 | + File: <nom_du_fichier> | |
| 454 | +*/ | |
| 455 | +``` | |
| 456 | + | |
| 457 | +Règles : | |
| 458 | +- **Aucun** fichier commité sans cet en-tête. | |
| 459 | +- Pour un connecteur, indiquer dans `Desc:` la **source/plateforme couverte**, le | |
| 460 | + **mode d'accès** (API / Firecrawl / Scrapfly / link-in-bio) et s'il fait de la | |
| 461 | + **découverte** ou de l'**enrichissement**. | |
| 462 | +- Vérifier la présence de l'en-tête en revue de code (idéalement hook de pré-commit). | |
| 463 | + | |
| 464 | +--- | |
| 465 | + | |
| 466 | +## 18. Tests & qualité | |
| 467 | + | |
| 468 | +- Chaque connecteur : **tests unitaires** de normalisation sur fixtures réelles | |
| 469 | + (réponses API, pages link-in-bio, profils publics figés). | |
| 470 | +- Tests de **résolution d'identité** : cas connus de comptes multi-plateformes + | |
| 471 | + cas-pièges d'**homonymes** à ne pas fusionner. | |
| 472 | +- **Validation de schéma** : toute fiche émise doit valider le schéma ; rejeter un | |
| 473 | + compte sans `url`/`confidence`. | |
| 474 | +- **Contrôle éthique automatisé** : vérifier qu'aucune donnée privée n'entre en base | |
| 475 | + et que les fiches `opted_out` restent exclues. | |
| 476 | +- **Monitoring** : alerte si une source cesse de retourner des données (blocage). | |
| 477 | + | |
| 478 | +--- | |
| 479 | + | |
| 480 | +## 19. Checklist — ajouter un connecteur / créateur | |
| 481 | + | |
| 482 | +1. [ ] Lire les connecteurs de référence dans le **dépôt Lou·Ka** (section 3). | |
| 483 | +2. [ ] Vérifier que la source/le créateur **n'est pas déjà couvert** (index/registre). | |
| 484 | +3. [ ] Découverte ou enrichissement ? Identifier la **plateforme/template** réutilisable. | |
| 485 | +4. [ ] Choisir le **mode d'accès** : API officielle > link-in-bio > Firecrawl > | |
| 486 | + Scrapfly (section 10), dans le respect des CGU. | |
| 487 | +5. [ ] Implémenter `fetch()` → `normalize()` → `emit()` (interface commune). | |
| 488 | +6. [ ] Mapper le schéma créateur + plateformes ; renseigner `confidence` par compte. | |
| 489 | +7. [ ] Brancher la **résolution d'identité** et la **déduplication** (section 12). | |
| 490 | +8. [ ] Appliquer les **règles de vie privée/éthique** (section 15) : public seulement, | |
| 491 | + opt-out respecté, régime mineurs, pas de données privées. | |
| 492 | +9. [ ] Gérer **fraîcheur, comptes disparus, inactifs, opt-out** (section 13). | |
| 493 | +10. [ ] Ajouter l'**en-tête d'auteur** (section 17). | |
| 494 | +11. [ ] Écrire les **tests** + fixtures, dont identité et contrôle éthique (section 18). | |
| 495 | +12. [ ] Enregistrer la source dans le **registre** avec sa cadence. | |
| 496 | +13. [ ] Valider volume, confiance des rattachements et qualité sur un premier run. | |
| 497 | + | |
| 498 | +--- | |
| 499 | + | |
| 500 | +## 20. Roadmap / MVP | |
| 501 | + | |
| 502 | +**MVP** : templatiser **un** connecteur **link-in-bio** (Linktree/Beacons) pour | |
| 503 | +remplir `platforms` d'un coup, + les **API officielles** YouTube et Twitch pour | |
| 504 | +l'enrichissement fiable. Découverte initiale via listes médias, agences et hashtags | |
| 505 | +québécois. Résolution d'identité de base (link-in-bio + liens croisés). UI d'annuaire | |
| 506 | +avec recherche/filtres par **niche, région, langue, plateforme, taille d'audience**, | |
| 507 | +et **page créateur** listant tous ses comptes. | |
| 508 | + | |
| 509 | +**Ensuite** : Instagram/TikTok (API officielles en priorité), Substack/Patreon, | |
| 510 | +métriques d'audience historisées, score de confiance affiné, opt-out en libre-service, | |
| 511 | +et cross-linking avec les autres sites `·Ka`. | |
| 512 | + | |
| 513 | +**Nord stratégique** : être l'**annuaire public de référence** des créateurs | |
| 514 | +québécois — exhaustif, cross-plateforme et respectueux de la vie privée — là où les | |
| 515 | +bases existantes sont payantes, généralistes et anglophones. | |
| 516 | + | |
| 517 | +--- | |
| 518 | + | |
| 519 | +*Fin de CLAUDE.md — Créa·Ka. Auteur : Simon-Pierre Boucher \<contact@spboucher.ai\>.* | |
added
README.md
+135 −0
@@ -0,0 +1,135 @@ | ||
| 1 | +<!-- | |
| 2 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | + File: README.md | |
| 4 | + Desc: Créa-Ka — agrégateur des créateurs de contenu québécois | |
| 5 | +--> | |
| 6 | +<div align="center"> | |
| 7 | + | |
| 8 | +# Créa·Ka | |
| 9 | + | |
| 10 | +**Tous les créateurs québécois. Tous leurs liens.** | |
| 11 | + | |
| 12 | +[](https://www.crea-ka.com) | |
| 13 | +[](https://www.crea-ka.com/stats) | |
| 14 | +[](https://www.crea-ka.com) | |
| 15 | + | |
| 16 | +[](https://www.python.org) | |
| 17 | +[](creaka/web.py) | |
| 18 | +[](frontend/src/index.template.html) | |
| 19 | +[](tests/) | |
| 20 | +[](creaka/connectors/) | |
| 21 | +[](https://www.crea-ka.com) | |
| 22 | + | |
| 23 | +Annuaire public, québécois, exhaustif et **cross-plateforme** des créateurs de | |
| 24 | +contenu — un croisement entre un annuaire cherchable et un « Linktree | |
| 25 | +collectif », membre de la famille **Groupe KA**. | |
| 26 | + | |
| 27 | +**https://www.crea-ka.com** | |
| 28 | + | |
| 29 | +</div> | |
| 30 | + | |
| 31 | +--- | |
| 32 | + | |
| 33 | +## Captures | |
| 34 | + | |
| 35 | +| Accueil (annuaire) | Page créateur | | |
| 36 | +|---|---| | |
| 37 | +|  |  | | |
| 38 | + | |
| 39 | +| Statistiques | Mobile (tab bar Lou·Ka) | | |
| 40 | +|---|---| | |
| 41 | +|  |  | | |
| 42 | + | |
| 43 | +## Ce que fait Créa·Ka | |
| 44 | + | |
| 45 | +Chaque créateur québécois a **une fiche canonique** qui regroupe **tous ses | |
| 46 | +comptes publics** — Instagram, TikTok, YouTube, Twitch, X, Facebook, Snapchat, | |
| 47 | +Kick, Substack, Patreon, OnlyFans, balado… — chacun avec son **URL canonique**, | |
| 48 | +ses **métriques** (abonnés, publications, j'aime, vues, épisodes) et son **score | |
| 49 | +de confiance de rattachement**. L'annuaire se filtre par **niche** (20), **région** | |
| 50 | +(17 régions administratives), **langue**, **plateforme** et **taille d'audience** | |
| 51 | +(nano/micro/macro/méga). Chaque créateur a sa page dédiée `/createur/{id}`. | |
| 52 | + | |
| 53 | +## Architecture (calquée sur Lou·Ka) | |
| 54 | + | |
| 55 | +``` | |
| 56 | +DÉCOUVERTE → EXTRACTION → RÉSOLUTION D'IDENTITÉ → NORMALISATION → DÉDUP. → SQLite → API/UI | |
| 57 | +``` | |
| 58 | + | |
| 59 | +| Module | Rôle | | |
| 60 | +|---|---| | |
| 61 | +| `creaka/schema.py` | fiche `Creator` + `PlatformAccount`, validation stricte (compte sans url/confidence = rejeté) | | |
| 62 | +| `creaka/identity.py` | signaux de rattachement §12.1 : `profil_source` 0.98 · `link_in_bio` 0.95 · `cross_link` 0.90 · `listes_medias`/`base_publique` 0.85 · `handle_match` 0.75 | | |
| 63 | +| `creaka/dedup.py` | fusion **uniquement** sur compte fort partagé — jamais sur le nom (homonymes) | | |
| 64 | +| `creaka/ethics.py` | purge des données privées, opt-out persistant, régime restreint des mineurs | | |
| 65 | +| `creaka/db.py` | SQLite WAL : fiches canoniques + index des comptes + journal des syncs | | |
| 66 | +| `creaka/web.py` | API FastAPI + frontend statique | | |
| 67 | + | |
| 68 | +### Connecteurs (registre auto-découvrant) | |
| 69 | + | |
| 70 | +| Connecteur | Famille | Accès | Signal | | |
| 71 | +|---|---|---|---| | |
| 72 | +| `listes-medias` | découverte | dataset local (palmarès médias publics croisés, 376 profils) | 0.85 | | |
| 73 | +| `wikidata-qc` | découverte | SPARQL Wikidata (nés/résidant au QC, handles curés) | 0.85 | | |
| 74 | +| `onlyqueb` | découverte | sitemap + JSON-LD (annuaire public, liens auto-déclarés) | 0.90 | | |
| 75 | +| `youtube-recherche` | découverte | recherche publique YouTube, ~120 requêtes × marqueur QC obligatoire | 0.98 | | |
| 76 | +| `balados-itunes` | découverte | **API officielle** iTunes/Apple Podcasts | 0.98 | | |
| 77 | +| `instagram-profil` | enrichissement | Scrapfly (profil public : abonnés, bio, **liens de bio** → cross-link) | — | | |
| 78 | +| `tiktok-profil` | enrichissement | Scrapfly (JSON d'hydratation public) | — | | |
| 79 | +| `youtube` | enrichissement | API officielle v3 si clé, sinon page publique | — | | |
| 80 | +| `twitch` | enrichissement | API officielle Helix (sauté sans clés) | — | | |
| 81 | +| `link-in-bio` | enrichissement | Linktree/Beacons/Bio.link… — **le levier nº 1** : une page → tous les comptes | 0.95 | | |
| 82 | + | |
| 83 | +La couverture **s'accumule à chaque passage** (watch quotidien) : la découverte | |
| 84 | +trouve de nouveaux créateurs, l'enrichissement rafraîchit les métriques et | |
| 85 | +révèle les comptes croisés via les liens de bio et les pages link-in-bio. | |
| 86 | + | |
| 87 | +## API publique | |
| 88 | + | |
| 89 | +``` | |
| 90 | +GET /api/creators?q&niche®ion&langue&plateforme&tier&sort&limit&offset | |
| 91 | +GET /api/creators/{id} GET /api/stats GET /api/taxonomies | |
| 92 | +GET /api/sources POST /api/optout GET /healthz | |
| 93 | +``` | |
| 94 | + | |
| 95 | +## Éthique — prime sur tout (CLAUDE.md §15) | |
| 96 | + | |
| 97 | +- **Profils publics et professionnels seulement** ; `ethics.scrub()` purge | |
| 98 | + téléphones, adresses civiques et courriels personnels avant tout stockage. | |
| 99 | +- **Opt-out immédiat et persistant** (`/retrait`) : fiche masquée sur-le-champ, | |
| 100 | + liste de suppression `data/optout.json`, **jamais ré-agrégée**. | |
| 101 | +- **Mineurs** : régime restreint automatique (année de naissance Wikidata) et | |
| 102 | + **exclusion de l'annuaire public**. | |
| 103 | +- **Score de confiance** sur chaque rattachement ; sous 0.50 → « à vérifier », | |
| 104 | + jamais publié. Mieux vaut un lien de moins qu'un lien erroné. | |
| 105 | +- Loi 25 : finalités documentées, accès/rectification/retrait via `/retrait`. | |
| 106 | + | |
| 107 | +## Développement | |
| 108 | + | |
| 109 | +```bash | |
| 110 | +python3 -m venv .venv && .venv/bin/pip install -r requirements.txt | |
| 111 | +cp .env.example .env # SCRAPFLY_KEY, YOUTUBE_API_KEY… | |
| 112 | +python3 -m pytest tests/ -q # 25 tests (identité, homonymes, éthique…) | |
| 113 | +python3 run.py sync # découverte + enrichissement | |
| 114 | +python3 run.py serve 8160 # API + frontend | |
| 115 | +python3 scripts/build_frontend.py # regénère frontend/dist (icônes SVG injectées) | |
| 116 | +python3 run.py optout "instagram:handle" # retrait manuel | |
| 117 | +``` | |
| 118 | + | |
| 119 | +## Déploiement | |
| 120 | + | |
| 121 | +Node **M3U96b** (`~/apps/crea-ka`) : PM2 `crea-ka-web` (:8160) + | |
| 122 | +`crea-ka-sync` (watch 24 h) + `crea-ka-ngrok` (**www.crea-ka.com**). | |
| 123 | +Redéploiement : `scripts/deploy-m3u96b.sh` · captures : `scripts/screenshots.sh`. | |
| 124 | + | |
| 125 | +--- | |
| 126 | + | |
| 127 | +<div align="center"> | |
| 128 | + | |
| 129 | +**Groupe KA** · [KA ID](https://www.groupe-ka.com) · [Lou-Ka](https://www.lou-ka.com) · | |
| 130 | +[Immo-Ka](https://www.immo-ka.com) · [Auto-Ka](https://www.auto-ka.com) · | |
| 131 | +[Food-Ka](https://www.food-ka.com) · [Fabri-Ka](https://www.fabri-ka.com) | |
| 132 | + | |
| 133 | +© Simon-Pierre Boucher — <contact@spboucher.ai> | |
| 134 | + | |
| 135 | +</div> | |
added
creaka/__init__.py
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/__init__.py | |
| 4 | +# Desc: Paquet Créa-Ka — agrégateur des créateurs de contenu québécois | |
| 5 | +# (calqué sur l'architecture du dépôt Lou-Ka) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Créa-Ka — annuaire public, québécois et cross-plateforme des créateurs. | |
| 8 | + | |
| 9 | +Pipeline : DÉCOUVERTE → EXTRACTION → RÉSOLUTION D'IDENTITÉ → NORMALISATION | |
| 10 | +→ DÉDUPLICATION → STOCKAGE → API/UI. Voir CLAUDE.md (source de vérité). | |
| 11 | +""" | |
| 12 | + | |
| 13 | +__version__ = "1.0.0" | |
added
creaka/connectors/__init__.py
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/__init__.py | |
| 4 | +# Desc: Registre AUTO-DÉCOUVRANT des connecteurs (calqué sur Lou-Ka) — | |
| 5 | +# tout module contenant une sous-classe de BaseConnector avec un | |
| 6 | +# source_id non vide est enregistré automatiquement | |
| 7 | +# ============================================================================== | |
| 8 | +from __future__ import annotations | |
| 9 | + | |
| 10 | +import importlib | |
| 11 | +import pkgutil | |
| 12 | +import sys | |
| 13 | + | |
| 14 | +from .base import BaseConnector | |
| 15 | + | |
| 16 | +CONNECTORS: dict[str, type[BaseConnector]] = {} | |
| 17 | + | |
| 18 | +for _mod in pkgutil.iter_modules(__path__): | |
| 19 | + if _mod.name in ("base", "__init__"): | |
| 20 | + continue | |
| 21 | + try: | |
| 22 | + module = importlib.import_module(f"{__name__}.{_mod.name}") | |
| 23 | + except Exception as exc: # un connecteur cassé ne bloque pas les autres | |
| 24 | + print(f"[crea-ka] connecteur '{_mod.name}' ignoré : {exc}", file=sys.stderr) | |
| 25 | + continue | |
| 26 | + for obj in vars(module).values(): | |
| 27 | + if (isinstance(obj, type) and issubclass(obj, BaseConnector) | |
| 28 | + and obj is not BaseConnector and getattr(obj, "source_id", "")): | |
| 29 | + CONNECTORS[obj.source_id] = obj | |
added
creaka/connectors/balados_itunes.py
+115 −0
@@ -0,0 +1,115 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/balados_itunes.py | |
| 4 | +# Desc: Connecteur DÉCOUVERTE — balados québécois via l'API de recherche | |
| 5 | +# iTunes/Apple Podcasts (OFFICIELLE, gratuite, sans clé). Palier 2-3 | |
| 6 | +# (§9). Filtre conservateur : marqueur québécois requis dans le nom du | |
| 7 | +# balado ou de l'artiste. | |
| 8 | +# ============================================================================== | |
| 9 | +"""Découverte des podcasteurs québécois via l'API iTunes Search. | |
| 10 | + | |
| 11 | +`itunes.apple.com/search?media=podcast` retourne pour chaque balado : nom, | |
| 12 | +artiste, pochette (artworkUrl600), genres, nb d'épisodes, flux RSS et page | |
| 13 | +Apple Podcasts. Le balado est le compte SOURCE de la fiche → profil_source. | |
| 14 | +""" | |
| 15 | +from __future__ import annotations | |
| 16 | + | |
| 17 | +from ..identity import account | |
| 18 | +from ..normalize import slugify | |
| 19 | +from ..schema import Creator | |
| 20 | +from .base import BaseConnector | |
| 21 | +from .youtube_recherche import is_quebec | |
| 22 | + | |
| 23 | +SEARCH_URL = "https://itunes.apple.com/search" | |
| 24 | + | |
| 25 | +QUERIES = [ | |
| 26 | + "québécois", "québécoise", "balado québec", "podcast québec", "montréal", | |
| 27 | + "humour québécois", "balado montréal", "saguenay", "gatineau", "sherbrooke", | |
| 28 | + "trois-rivières", "ville de québec balado", "balado gaspésie", "mauricie", | |
| 29 | + "balado estrie", "rimouski", "abitibi", "lévis balado", "laval balado", | |
| 30 | + "balado", "podcast montréal", "balado hockey", "canadiens de montréal", | |
| 31 | + "histoire du québec", "crime québec", "balado finance québec", | |
| 32 | + "balado maman québec", "balado couple québec", "balado santé québec", | |
| 33 | + "balado sport québec", "balado politique québec", "balado culture québec", | |
| 34 | + "balado musique québec", "balado cinéma québec", "balado jeux vidéo québec", | |
| 35 | + "balado plein air québec", "balado immobilier québec", "balado entrepreneur québec", | |
| 36 | + "outaouais", "lanaudière", "laurentides balado", "montérégie", | |
| 37 | + "côte-nord balado", "charlevoix balado", "beauce balado", | |
| 38 | + "drummondville", "victoriaville balado", "granby balado", "chicoutimi", | |
| 39 | + # longue traîne : villes + créneaux (nano/micro podcasteurs locaux) | |
| 40 | + "longueuil", "terrebonne balado", "repentigny balado", "saint-jérôme", | |
| 41 | + "saint-hyacinthe", "shawinigan", "rouyn-noranda", "val-d'or", | |
| 42 | + "sept-îles", "baie-comeau", "alma balado", "matane", "rivière-du-loup", | |
| 43 | + "sorel-tracy", "magog balado", "joliette balado", "châteauguay", | |
| 44 | + "balado chasse québec", "balado pêche québec", "balado agriculture québec", | |
| 45 | + "balado construction québec", "balado véhicule québec", "balado techno québec", | |
| 46 | + "balado ados québec", "balado église québec", "balado histoire vraie québec", | |
| 47 | + "balado humour absurde québec", "balado société québec", "balado femmes québec", | |
| 48 | + "balado papa québec", "balado éducation québec", "balado science québec", | |
| 49 | + "balado littérature québec", "balado poésie québec", "balado bd québec", | |
| 50 | + "balado jeux de société québec", "balado vin québec", "balado bière québec", | |
| 51 | +] | |
| 52 | + | |
| 53 | +_GENRE_NICHE = { | |
| 54 | + "Comedy": "humour", "Food": "bouffe-resto", "Sports": "sport-fitness", | |
| 55 | + "Music": "musique", "Business": "finance-affaires", "Investing": "finance-affaires", | |
| 56 | + "Health & Fitness": "sante-mieux-etre", "Education": "education", | |
| 57 | + "True Crime": "actualite-opinion", "News": "actualite-opinion", | |
| 58 | + "Politics": "actualite-opinion", "Society & Culture": "actualite-opinion", | |
| 59 | + "TV & Film": "actualite-opinion", "Leisure": "gaming", | |
| 60 | + "Video Games": "gaming", "Technology": "tech", "Arts": "arts", | |
| 61 | + "Parenting": "famille-parentalite", "Kids & Family": "famille-parentalite", | |
| 62 | + "Fiction": "arts", "Religion & Spirituality": "sante-mieux-etre", | |
| 63 | + "Science": "education", "History": "education", "Wilderness": "plein-air", | |
| 64 | +} | |
| 65 | + | |
| 66 | + | |
| 67 | +class BaladosItunesConnector(BaseConnector): | |
| 68 | + source_id = "balados-itunes" | |
| 69 | + kind = "discovery" | |
| 70 | + request_delay = 1.5 | |
| 71 | + | |
| 72 | + def fetch(self) -> list[Creator]: | |
| 73 | + creators: list[Creator] = [] | |
| 74 | + seen: set[int] = set() | |
| 75 | + for query in QUERIES: | |
| 76 | + try: | |
| 77 | + resp = self.get(SEARCH_URL, params={ | |
| 78 | + "media": "podcast", "term": query, "country": "CA", | |
| 79 | + "limit": 200}) | |
| 80 | + results = resp.json().get("results") or [] | |
| 81 | + except Exception: | |
| 82 | + continue | |
| 83 | + for r in results: | |
| 84 | + cid = r.get("collectionId") | |
| 85 | + name = (r.get("collectionName") or "").strip() | |
| 86 | + artist = (r.get("artistName") or "").strip() | |
| 87 | + if not cid or cid in seen or not name: | |
| 88 | + continue | |
| 89 | + # filtre conservateur : marqueur QC dans le nom du balado/artiste | |
| 90 | + if not is_quebec(f"{name} {artist}"): | |
| 91 | + continue | |
| 92 | + seen.add(cid) | |
| 93 | + url = r.get("collectionViewUrl") or r.get("feedUrl") or "" | |
| 94 | + if not url: | |
| 95 | + continue | |
| 96 | + acc = account("podcast", str(cid), "profil_source", url=url) | |
| 97 | + acc.metrics = {k: v for k, v in { | |
| 98 | + "episodes": r.get("trackCount"), | |
| 99 | + "genre": r.get("primaryGenreName"), | |
| 100 | + }.items() if v} | |
| 101 | + niche = _GENRE_NICHE.get(r.get("primaryGenreName") or "", | |
| 102 | + "actualite-opinion") | |
| 103 | + creators.append(Creator( | |
| 104 | + source=self.source_id, | |
| 105 | + external_id=str(cid), | |
| 106 | + display_name=name, | |
| 107 | + bio=f"Balado de {artist}." if artist and artist != name else "", | |
| 108 | + niches=[niche], | |
| 109 | + creator_type="podcasteur", | |
| 110 | + avatar_url=r.get("artworkUrl600") or r.get("artworkUrl100"), | |
| 111 | + platforms=[acc], | |
| 112 | + notes="Référencé sur Apple Podcasts (API de recherche officielle).", | |
| 113 | + source_ids=[f"{self.source_id}:{cid}"], | |
| 114 | + )) | |
| 115 | + return creators | |
added
creaka/connectors/base.py
+130 −0
@@ -0,0 +1,130 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/base.py | |
| 4 | +# Desc: Classe de base des connecteurs + backends de fetch (requests direct, | |
| 5 | +# Firecrawl, Scrapfly) — calquée sur louka/connectors/base.py | |
| 6 | +# ============================================================================== | |
| 7 | +from __future__ import annotations | |
| 8 | + | |
| 9 | +import json | |
| 10 | +import os | |
| 11 | +import time | |
| 12 | + | |
| 13 | +import requests | |
| 14 | + | |
| 15 | +USER_AGENT = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " | |
| 16 | + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 " | |
| 17 | + "CreaKaBot/1.0 (+https://www.crea-ka.com/bot; contact@spboucher.ai)") | |
| 18 | + | |
| 19 | +FIRECRAWL_API = "https://api.firecrawl.dev/v1/scrape" | |
| 20 | +SCRAPFLY_API = "https://api.scrapfly.io/scrape" | |
| 21 | + | |
| 22 | + | |
| 23 | +class BaseConnector: | |
| 24 | + """Un connecteur = un adaptateur propre à une source (CLAUDE.md §8). | |
| 25 | + | |
| 26 | + Deux familles : | |
| 27 | + - `kind = "discovery"` : `fetch()` retourne la liste de fiches `Creator` | |
| 28 | + découvertes (listes médias, hashtags, agences…). | |
| 29 | + - `kind = "enrichment"` : `enrich(creators)` reçoit les fiches existantes | |
| 30 | + et retourne celles qu'il a enrichies (profil, abonnés, liens). | |
| 31 | + | |
| 32 | + Le pipeline (ingest.py) s'occupe du diff avec la base de données. | |
| 33 | + Chaque connecteur documente en tête son MODE D'ACCÈS (API / Firecrawl / | |
| 34 | + Scrapfly / link-in-bio) et sa famille (§10, §17). | |
| 35 | + """ | |
| 36 | + | |
| 37 | + source_id: str = "" | |
| 38 | + kind: str = "discovery" # discovery | enrichment | |
| 39 | + request_delay: float = 0.8 # politesse entre requêtes (§15 rate-limit) | |
| 40 | + timeout: int = 30 | |
| 41 | + | |
| 42 | + def __init__(self) -> None: | |
| 43 | + self.session = requests.Session() | |
| 44 | + self.session.headers["User-Agent"] = USER_AGENT | |
| 45 | + self._last_request = 0.0 | |
| 46 | + | |
| 47 | + # -- backends ------------------------------------------------------------- | |
| 48 | + def _throttle(self) -> None: | |
| 49 | + wait = self.request_delay - (time.time() - self._last_request) | |
| 50 | + if wait > 0: | |
| 51 | + time.sleep(wait) | |
| 52 | + | |
| 53 | + def get(self, url: str, **kw) -> requests.Response: | |
| 54 | + """GET direct avec throttling poli.""" | |
| 55 | + self._throttle() | |
| 56 | + resp = self.session.get(url, timeout=self.timeout, **kw) | |
| 57 | + self._last_request = time.time() | |
| 58 | + resp.raise_for_status() | |
| 59 | + return resp | |
| 60 | + | |
| 61 | + def post(self, url: str, **kw) -> requests.Response: | |
| 62 | + """POST direct avec throttling poli (APIs officielles).""" | |
| 63 | + self._throttle() | |
| 64 | + resp = self.session.post(url, timeout=self.timeout, **kw) | |
| 65 | + self._last_request = time.time() | |
| 66 | + resp.raise_for_status() | |
| 67 | + return resp | |
| 68 | + | |
| 69 | + def get_rendered(self, url: str) -> str: | |
| 70 | + """HTML rendu (JavaScript exécuté) via Firecrawl. | |
| 71 | + | |
| 72 | + Nécessite FIRECRAWL_API_KEY dans l'environnement (.env). Pour les | |
| 73 | + pages publiques relativement ouvertes (agences, listes, blogues). | |
| 74 | + """ | |
| 75 | + key = os.environ.get("FIRECRAWL_API_KEY") | |
| 76 | + if not key: | |
| 77 | + raise RuntimeError("FIRECRAWL_API_KEY manquant (voir .env)") | |
| 78 | + resp = requests.post( | |
| 79 | + FIRECRAWL_API, | |
| 80 | + json={"url": url, "formats": ["html"]}, | |
| 81 | + headers={"Authorization": f"Bearer {key}"}, | |
| 82 | + timeout=90, | |
| 83 | + ) | |
| 84 | + resp.raise_for_status() | |
| 85 | + data = resp.json() | |
| 86 | + return (data.get("data") or {}).get("html", "") | |
| 87 | + | |
| 88 | + def scrapfly(self, url: str, render_js: bool = True, asp: bool = True, | |
| 89 | + rendering_wait: int = 0, country: str = "ca", | |
| 90 | + wait_for_selector: str | None = None, | |
| 91 | + headers: dict | None = None) -> dict: | |
| 92 | + """Appel Scrapfly — retourne le dict `result` (content, status_code…). | |
| 93 | + | |
| 94 | + À MINIMISER (§10) : uniquement les plateformes hostiles au scraping, | |
| 95 | + faute d'API officielle, dans le respect des CGU (§15). | |
| 96 | + """ | |
| 97 | + key = os.environ.get("SCRAPFLY_KEY") | |
| 98 | + if not key: | |
| 99 | + raise RuntimeError("SCRAPFLY_KEY manquant (voir .env)") | |
| 100 | + params: dict = {"key": key, "url": url, "country": country} | |
| 101 | + if asp: | |
| 102 | + params["asp"] = "true" | |
| 103 | + if render_js: | |
| 104 | + params["render_js"] = "true" | |
| 105 | + if rendering_wait: | |
| 106 | + params["rendering_wait"] = rendering_wait | |
| 107 | + if wait_for_selector: | |
| 108 | + params["wait_for_selector"] = wait_for_selector | |
| 109 | + if headers: | |
| 110 | + for k, v in headers.items(): | |
| 111 | + params[f"headers[{k}]"] = v | |
| 112 | + self._throttle() | |
| 113 | + resp = requests.get(SCRAPFLY_API, params=params, timeout=120) | |
| 114 | + self._last_request = time.time() | |
| 115 | + resp.raise_for_status() | |
| 116 | + return resp.json().get("result") or {} | |
| 117 | + | |
| 118 | + # -- interface ------------------------------------------------------------ | |
| 119 | + def fetch(self) -> list: | |
| 120 | + """Découverte : liste complète des fiches `Creator` de la source.""" | |
| 121 | + raise NotImplementedError | |
| 122 | + | |
| 123 | + def enrich(self, creators: list) -> list: | |
| 124 | + """Enrichissement : retourne les fiches modifiées (sous-ensemble).""" | |
| 125 | + raise NotImplementedError | |
| 126 | + | |
| 127 | + | |
| 128 | +def load_json(text: str): | |
| 129 | + """json.loads tolérant (BOM, espaces).""" | |
| 130 | + return json.loads(text.strip().lstrip("")) | |
added
creaka/connectors/instagram.py
+107 −0
@@ -0,0 +1,107 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/instagram.py | |
| 4 | +# Desc: Connecteur ENRICHISSEMENT Instagram — profil public par créateur : | |
| 5 | +# abonnés, badge vérifié, bio, liens de bio (→ découverte croisée des | |
| 6 | +# autres plateformes + page link-in-bio). Mode d'accès : Scrapfly | |
| 7 | +# (plateforme hostile au scraping, pas d'API publique de lecture) — | |
| 8 | +# palier 4 du catalogue (§9), CGU/rate-limit respectés (§15). | |
| 9 | +# ============================================================================== | |
| 10 | +"""Enrichissement Instagram, créateur par créateur. | |
| 11 | + | |
| 12 | +Pour chaque fiche ayant un compte Instagram rattaché, on lit le profil PUBLIC | |
| 13 | +via l'endpoint web_profile_info (JSON servi par le web public d'Instagram), | |
| 14 | +à travers Scrapfly (anti-bot). On en tire : | |
| 15 | +- followers / verified / bio du compte Instagram (métriques, §13-14) ; | |
| 16 | +- `external_url` + `bio_links` : si c'est une page link-in-bio connue → | |
| 17 | + `link_in_bio_url` (traitée ensuite par le connecteur link-in-bio) ; si c'est | |
| 18 | + un autre réseau → nouveau compte rattaché avec signal `cross_link` (0.90, | |
| 19 | + §12.1 : le créateur pointe LUI-MÊME vers cet autre compte). | |
| 20 | + | |
| 21 | +Uniquement des profils publics ; un profil privé/introuvable est ignoré. | |
| 22 | +""" | |
| 23 | +from __future__ import annotations | |
| 24 | + | |
| 25 | +import json | |
| 26 | + | |
| 27 | +from ..identity import account, merge_accounts | |
| 28 | +from ..normalize import parse_count, platform_from_url | |
| 29 | +from ..schema import Creator, now_iso | |
| 30 | +from .base import BaseConnector | |
| 31 | +from .linkinbio import is_supported as is_linkinbio | |
| 32 | + | |
| 33 | +PROFILE_API = ("https://i.instagram.com/api/v1/users/web_profile_info/" | |
| 34 | + "?username={h}") | |
| 35 | +IG_APP_ID = "936619743392459" # app id du web public instagram.com | |
| 36 | + | |
| 37 | + | |
| 38 | +class InstagramConnector(BaseConnector): | |
| 39 | + source_id = "instagram-profil" | |
| 40 | + kind = "enrichment" | |
| 41 | + request_delay = 1.5 | |
| 42 | + max_profiles = 700 # garde-fou par passage | |
| 43 | + | |
| 44 | + def fetch_profile(self, handle: str) -> dict | None: | |
| 45 | + """Profil public JSON d'un handle Instagram (None si privé/introuvable).""" | |
| 46 | + result = self.scrapfly(PROFILE_API.format(h=handle), render_js=False, | |
| 47 | + headers={"x-ig-app-id": IG_APP_ID, | |
| 48 | + "accept": "application/json"}) | |
| 49 | + if result.get("status_code") != 200: | |
| 50 | + return None | |
| 51 | + try: | |
| 52 | + data = json.loads(result.get("content") or "{}") | |
| 53 | + except Exception: | |
| 54 | + return None | |
| 55 | + user = ((data.get("data") or {}).get("user")) or None | |
| 56 | + if not user or user.get("is_private"): | |
| 57 | + return None | |
| 58 | + return user | |
| 59 | + | |
| 60 | + def enrich(self, creators: list[Creator]) -> list[Creator]: | |
| 61 | + enriched: list[Creator] = [] | |
| 62 | + fetched = 0 | |
| 63 | + for cr in creators: | |
| 64 | + ig = next((a for a in cr.platforms if a.platform == "instagram"), None) | |
| 65 | + if ig is None or fetched >= self.max_profiles: | |
| 66 | + continue | |
| 67 | + try: | |
| 68 | + user = self.fetch_profile(ig.handle) | |
| 69 | + except Exception: | |
| 70 | + continue | |
| 71 | + fetched += 1 | |
| 72 | + if not user: | |
| 73 | + continue | |
| 74 | + # métriques du compte lui-même (étendues : la référence, pas le minimum) | |
| 75 | + ig.followers = parse_count( | |
| 76 | + (user.get("edge_followed_by") or {}).get("count")) or ig.followers | |
| 77 | + ig.verified = bool(user.get("is_verified")) | |
| 78 | + ig.last_checked = now_iso() | |
| 79 | + extra = { | |
| 80 | + "following": (user.get("edge_follow") or {}).get("count"), | |
| 81 | + "posts": (user.get("edge_owner_to_timeline_media") or {}).get("count"), | |
| 82 | + "category": user.get("category_name"), | |
| 83 | + "is_business": user.get("is_business_account"), | |
| 84 | + } | |
| 85 | + ig.metrics.update({k: v for k, v in extra.items() if v not in (None, "")}) | |
| 86 | + if not cr.avatar_url and user.get("profile_pic_url_hd"): | |
| 87 | + cr.avatar_url = user["profile_pic_url_hd"] | |
| 88 | + if not cr.bio and user.get("biography"): | |
| 89 | + cr.bio = user["biography"] | |
| 90 | + # liens de bio → découverte croisée (§12.1 signal 2) | |
| 91 | + new_accounts = [] | |
| 92 | + links = [user.get("external_url") or ""] | |
| 93 | + links += [(b.get("url") or "") for b in (user.get("bio_links") or [])] | |
| 94 | + for url in links: | |
| 95 | + if not url: | |
| 96 | + continue | |
| 97 | + if is_linkinbio(url) and not cr.link_in_bio_url: | |
| 98 | + cr.link_in_bio_url = url | |
| 99 | + continue | |
| 100 | + hit = platform_from_url(url) | |
| 101 | + if hit and hit[0] != "instagram": | |
| 102 | + new_accounts.append( | |
| 103 | + account(hit[0], hit[1], "cross_link", url=url).finalize()) | |
| 104 | + if new_accounts: | |
| 105 | + cr.platforms = merge_accounts(cr.platforms, new_accounts) | |
| 106 | + enriched.append(cr) | |
| 107 | + return enriched | |
added
creaka/connectors/linkinbio.py
+132 −0
@@ -0,0 +1,132 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/linkinbio.py | |
| 4 | +# Desc: Connecteur ENRICHISSEMENT générique « link-in-bio » (templatisé) — | |
| 5 | +# Linktree, Beacons, Bio.link, Milkshake, Campsite, Snipfeed. | |
| 6 | +# Mode d'accès : requêtes directes (pages publiques SSR), repli | |
| 7 | +# Firecrawl. Palier 1 du catalogue (§9) — le levier nº 1 : une page | |
| 8 | +# de liens révèle D'UN COUP tous les comptes d'un créateur. | |
| 9 | +# ============================================================================== | |
| 10 | +"""Enrichissement par pages de liens (link-in-bio). | |
| 11 | + | |
| 12 | +Pour chaque créateur dont `link_in_bio_url` est connue (déclarée par le | |
| 13 | +créateur dans une de ses bios — jamais devinée), on récupère la page et on | |
| 14 | +en extrait les comptes de plateformes. Signal d'identité : `link_in_bio` | |
| 15 | +(le créateur liste lui-même ses comptes) → confidence 0.95, le plus fort | |
| 16 | +(§12.1). | |
| 17 | + | |
| 18 | +Templatisé : chaque service a son extracteur ; le repli générique parcourt | |
| 19 | +les ancres de la page et classe chaque URL via normalize.platform_from_url. | |
| 20 | +""" | |
| 21 | +from __future__ import annotations | |
| 22 | + | |
| 23 | +import json | |
| 24 | +import re | |
| 25 | + | |
| 26 | +from ..identity import account, merge_accounts | |
| 27 | +from ..normalize import platform_from_url | |
| 28 | +from ..schema import Creator | |
| 29 | +from .base import BaseConnector | |
| 30 | + | |
| 31 | +# services couverts par le gabarit (§9 palier 1) | |
| 32 | +SERVICES = ("linktr.ee", "beacons.ai", "bio.link", "msha.ke", | |
| 33 | + "campsite.bio", "snipfeed.co", "linkin.bio") | |
| 34 | + | |
| 35 | +_NEXT_DATA_RE = re.compile( | |
| 36 | + r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>', re.S) | |
| 37 | +_HREF_RE = re.compile(r'href="([^"]+)"', re.I) | |
| 38 | + | |
| 39 | + | |
| 40 | +def is_supported(url: str | None) -> bool: | |
| 41 | + return bool(url) and any(s in url for s in SERVICES) | |
| 42 | + | |
| 43 | + | |
| 44 | +def parse_links(html: str) -> list[str]: | |
| 45 | + """Extrait toutes les URLs sortantes d'une page link-in-bio. | |
| 46 | + | |
| 47 | + Linktree/Beacons sont des apps Next.js : les liens vivent dans le JSON | |
| 48 | + __NEXT_DATA__ (plus fiable que le DOM). Repli : ancres du HTML. | |
| 49 | + """ | |
| 50 | + urls: list[str] = [] | |
| 51 | + m = _NEXT_DATA_RE.search(html) | |
| 52 | + if m: | |
| 53 | + try: | |
| 54 | + data = json.loads(m.group(1)) | |
| 55 | + urls.extend(_walk_urls(data)) | |
| 56 | + except Exception: | |
| 57 | + pass | |
| 58 | + for href in _HREF_RE.findall(html): | |
| 59 | + if href.startswith("http"): | |
| 60 | + urls.append(href) | |
| 61 | + # dédupliquer en conservant l'ordre (l'ordre de la page = priorité du créateur) | |
| 62 | + seen: set[str] = set() | |
| 63 | + out = [] | |
| 64 | + for u in urls: | |
| 65 | + u = u.split("?utm")[0].rstrip("/") | |
| 66 | + if u not in seen: | |
| 67 | + seen.add(u) | |
| 68 | + out.append(u) | |
| 69 | + return out | |
| 70 | + | |
| 71 | + | |
| 72 | +def _walk_urls(node) -> list[str]: | |
| 73 | + """Parcourt récursivement le JSON Next.js et récolte toute valeur 'url'.""" | |
| 74 | + found: list[str] = [] | |
| 75 | + if isinstance(node, dict): | |
| 76 | + for k, v in node.items(): | |
| 77 | + if k in ("url", "href") and isinstance(v, str) and v.startswith("http"): | |
| 78 | + found.append(v) | |
| 79 | + else: | |
| 80 | + found.extend(_walk_urls(v)) | |
| 81 | + elif isinstance(node, list): | |
| 82 | + for item in node: | |
| 83 | + found.extend(_walk_urls(item)) | |
| 84 | + return found | |
| 85 | + | |
| 86 | + | |
| 87 | +def extract_accounts(html: str) -> list: | |
| 88 | + """Page link-in-bio → comptes de plateformes (signal fort link_in_bio).""" | |
| 89 | + accounts = [] | |
| 90 | + seen: set[str] = set() | |
| 91 | + for url in parse_links(html): | |
| 92 | + hit = platform_from_url(url) | |
| 93 | + if not hit: | |
| 94 | + continue | |
| 95 | + platform, handle = hit | |
| 96 | + key = f"{platform}:{handle}" | |
| 97 | + if key in seen: | |
| 98 | + continue | |
| 99 | + seen.add(key) | |
| 100 | + accounts.append(account(platform, handle, "link_in_bio", url=url)) | |
| 101 | + return accounts | |
| 102 | + | |
| 103 | + | |
| 104 | +class LinkInBioConnector(BaseConnector): | |
| 105 | + source_id = "link-in-bio" | |
| 106 | + kind = "enrichment" | |
| 107 | + request_delay = 1.2 | |
| 108 | + max_pages = 500 # garde-fou par passage | |
| 109 | + | |
| 110 | + def enrich(self, creators: list[Creator]) -> list[Creator]: | |
| 111 | + enriched: list[Creator] = [] | |
| 112 | + fetched = 0 | |
| 113 | + for cr in creators: | |
| 114 | + if fetched >= self.max_pages: | |
| 115 | + break | |
| 116 | + if not is_supported(cr.link_in_bio_url): | |
| 117 | + continue | |
| 118 | + try: | |
| 119 | + html = self.get(cr.link_in_bio_url).text | |
| 120 | + except Exception: | |
| 121 | + try: | |
| 122 | + html = self.get_rendered(cr.link_in_bio_url) | |
| 123 | + except Exception: | |
| 124 | + continue | |
| 125 | + fetched += 1 | |
| 126 | + accounts = extract_accounts(html) | |
| 127 | + if not accounts: | |
| 128 | + continue | |
| 129 | + cr.platforms = merge_accounts(cr.platforms, [a.finalize() | |
| 130 | + for a in accounts]) | |
| 131 | + enriched.append(cr) | |
| 132 | + return enriched | |
added
creaka/connectors/listes_medias.py
+89 −0
@@ -0,0 +1,89 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/listes_medias.py | |
| 4 | +# Desc: Connecteur DÉCOUVERTE — listes & palmarès médias québécois compilés | |
| 5 | +# (dataset local data/seed/qc_influenceurs.json, croisement de listes | |
| 6 | +# publiques : billie, frank, metricool, Favikon QC…). Mode d'accès : | |
| 7 | +# dataset local (aucune requête réseau). Palier 3 du catalogue (§9). | |
| 8 | +# ============================================================================== | |
| 9 | +"""Découverte initiale : 376 créateurs québécois issus de listes médias publiques. | |
| 10 | + | |
| 11 | +Le dataset (compilé pour influenceursqc.com à partir de palmarès publics — | |
| 12 | +tops médias, classements Favikon/metricool, articles de presse) fournit pour | |
| 13 | +chaque créateur : nom public, handles par plateforme, abonnés approximatifs, | |
| 14 | +catégorie et localisation DÉCLARÉE. Signal d'identité : `listes_medias` | |
| 15 | +(plusieurs listes publiques concordantes) → confidence 0.85 (§12.1). | |
| 16 | + | |
| 17 | +Aucune donnée privée : uniquement des profils publics de créateurs qui se | |
| 18 | +présentent publiquement comme tels (§15). | |
| 19 | +""" | |
| 20 | +from __future__ import annotations | |
| 21 | + | |
| 22 | +import json | |
| 23 | +from pathlib import Path | |
| 24 | + | |
| 25 | +from ..identity import account | |
| 26 | +from ..normalize import map_region, slugify | |
| 27 | +from ..schema import Creator | |
| 28 | +from .base import BaseConnector | |
| 29 | + | |
| 30 | +SEED_PATH = Path(__file__).resolve().parent.parent.parent / "data" / "seed" / \ | |
| 31 | + "qc_influenceurs.json" | |
| 32 | + | |
| 33 | +# clés du dataset → (plateforme canonique, clé de métrique) | |
| 34 | +_PLATFORM_KEYS = { | |
| 35 | + "instagram": "instagram_followers", | |
| 36 | + "tiktok": "tiktok_followers", | |
| 37 | + "youtube": "youtube_subscribers", | |
| 38 | + "facebook": "facebook_followers", | |
| 39 | + "x": "x_followers", | |
| 40 | + "twitch": "twitch_followers", | |
| 41 | +} | |
| 42 | + | |
| 43 | + | |
| 44 | +class ListesMediasConnector(BaseConnector): | |
| 45 | + source_id = "listes-medias" | |
| 46 | + kind = "discovery" | |
| 47 | + | |
| 48 | + def fetch(self) -> list[Creator]: | |
| 49 | + if not SEED_PATH.exists(): | |
| 50 | + raise RuntimeError(f"dataset seed introuvable : {SEED_PATH}") | |
| 51 | + raw = json.loads(SEED_PATH.read_text(encoding="utf-8")) | |
| 52 | + creators: list[Creator] = [] | |
| 53 | + for entry in raw: | |
| 54 | + try: | |
| 55 | + cr = self._normalize(entry) | |
| 56 | + if cr is not None: | |
| 57 | + creators.append(cr) | |
| 58 | + except Exception: | |
| 59 | + continue # une entrée malformée ne bloque pas la source | |
| 60 | + return creators | |
| 61 | + | |
| 62 | + def _normalize(self, entry: dict) -> Creator | None: | |
| 63 | + name = (entry.get("name") or "").strip() | |
| 64 | + if not name: | |
| 65 | + return None | |
| 66 | + handles = entry.get("handles") or {} | |
| 67 | + metrics = entry.get("metrics") or {} | |
| 68 | + accounts = [] | |
| 69 | + for platform, metric_key in _PLATFORM_KEYS.items(): | |
| 70 | + handle = handles.get(platform) | |
| 71 | + if not handle: | |
| 72 | + continue | |
| 73 | + accounts.append(account(platform, handle, "listes_medias", | |
| 74 | + followers=metrics.get(metric_key))) | |
| 75 | + if not accounts: | |
| 76 | + return None | |
| 77 | + region, city = map_region(entry.get("location")) | |
| 78 | + slug = slugify(name) | |
| 79 | + return Creator( | |
| 80 | + source=self.source_id, | |
| 81 | + external_id=slug, | |
| 82 | + display_name=name, | |
| 83 | + niches=[entry.get("category") or "autre"], | |
| 84 | + region=region, | |
| 85 | + city=city, | |
| 86 | + platforms=accounts, | |
| 87 | + notes=(entry.get("notes") or "")[:1200], | |
| 88 | + source_ids=[f"{self.source_id}:{slug}"], | |
| 89 | + ) | |
added
creaka/connectors/onlyqueb.py
+104 −0
@@ -0,0 +1,104 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/onlyqueb.py | |
| 4 | +# Desc: Connecteur DÉCOUVERTE — OnlyQueb (onlyqueb.com), annuaire public de | |
| 5 | +# créatrices/créateurs québécois sur abonnement. Mode d'accès : requêtes | |
| 6 | +# directes (sitemap.xml + JSON-LD des pages profil, SSR). Palier 3 (§9). | |
| 7 | +# Les liens `sameAs` sont AUTO-DÉCLARÉS par la personne sur son profil | |
| 8 | +# → signal cross_link (0.90, §12.1). | |
| 9 | +# ============================================================================== | |
| 10 | +"""Découverte via le hub OnlyQueb. | |
| 11 | + | |
| 12 | +Chaque page profil expose un JSON-LD schema.org ProfilePage propre : | |
| 13 | +nom public, photo de profil publique et liste `sameAs` des comptes | |
| 14 | +(OnlyFans, Instagram, TikTok, X…). Adultes uniquement (plateforme 18+). | |
| 15 | +On n'agrège que ce que la personne affiche elle-même sur son profil (§15). | |
| 16 | +""" | |
| 17 | +from __future__ import annotations | |
| 18 | + | |
| 19 | +import json | |
| 20 | +import re | |
| 21 | + | |
| 22 | +from ..identity import account | |
| 23 | +from ..normalize import platform_from_url, slugify | |
| 24 | +from ..schema import Creator | |
| 25 | +from .base import BaseConnector | |
| 26 | + | |
| 27 | +SITEMAP_URL = "https://onlyqueb.com/sitemap.xml" | |
| 28 | +_LOC_RE = re.compile(r"<loc>([^<]+)</loc>") | |
| 29 | +_LD_RE = re.compile(r'application/ld\+json"[^>]*>(.*?)</script>', re.S) | |
| 30 | +_EXCLUDED = ("/en", "/categories", "/explore", "/seo", "/signup", "/blog") | |
| 31 | + | |
| 32 | + | |
| 33 | +def parse_profile_ld(html: str) -> dict | None: | |
| 34 | + """Page profil → {name, image, links[]} depuis le JSON-LD (None si absent).""" | |
| 35 | + for blob in _LD_RE.findall(html): | |
| 36 | + try: | |
| 37 | + data = json.loads(blob) | |
| 38 | + except Exception: | |
| 39 | + continue | |
| 40 | + if data.get("@type") != "ProfilePage": | |
| 41 | + continue | |
| 42 | + person = data.get("mainEntity") or {} | |
| 43 | + if person.get("@type") != "Person": | |
| 44 | + continue | |
| 45 | + return {"name": (person.get("name") or "").strip(), | |
| 46 | + "image": person.get("image") or None, | |
| 47 | + "links": [u for u in (person.get("sameAs") or []) | |
| 48 | + if isinstance(u, str)]} | |
| 49 | + return None | |
| 50 | + | |
| 51 | + | |
| 52 | +class OnlyQuebConnector(BaseConnector): | |
| 53 | + source_id = "onlyqueb" | |
| 54 | + kind = "discovery" | |
| 55 | + request_delay = 1.0 | |
| 56 | + max_profiles = 600 # garde-fou par passage | |
| 57 | + | |
| 58 | + def _profile_urls(self) -> list[str]: | |
| 59 | + xml = self.get(SITEMAP_URL).text | |
| 60 | + urls = [] | |
| 61 | + for u in _LOC_RE.findall(xml): | |
| 62 | + path = u.split("onlyqueb.com", 1)[-1] | |
| 63 | + if path and path != "/" and path.count("/") == 1 \ | |
| 64 | + and not any(path.startswith(e) for e in _EXCLUDED): | |
| 65 | + urls.append(u) | |
| 66 | + return urls[:self.max_profiles] | |
| 67 | + | |
| 68 | + def fetch(self) -> list[Creator]: | |
| 69 | + creators: list[Creator] = [] | |
| 70 | + for url in self._profile_urls(): | |
| 71 | + try: | |
| 72 | + html = self.get(url).text | |
| 73 | + except Exception: | |
| 74 | + continue | |
| 75 | + prof = parse_profile_ld(html) | |
| 76 | + if not prof or not prof["name"]: | |
| 77 | + continue | |
| 78 | + accounts = [] | |
| 79 | + seen: set[str] = set() | |
| 80 | + for link in prof["links"]: | |
| 81 | + hit = platform_from_url(link) | |
| 82 | + if not hit: | |
| 83 | + continue | |
| 84 | + platform, handle = hit | |
| 85 | + if f"{platform}:{handle}" in seen: | |
| 86 | + continue | |
| 87 | + seen.add(f"{platform}:{handle}") | |
| 88 | + accounts.append(account(platform, handle, "cross_link", url=link)) | |
| 89 | + if not accounts: | |
| 90 | + continue # sans lien vérifiable, pas de fiche (annuaire de liens) | |
| 91 | + slug = slugify(prof["name"]) | |
| 92 | + creators.append(Creator( | |
| 93 | + source=self.source_id, | |
| 94 | + external_id=slug, | |
| 95 | + display_name=prof["name"], | |
| 96 | + niches=["autre"], | |
| 97 | + creator_type="influenceur", | |
| 98 | + avatar_url=prof["image"], | |
| 99 | + platforms=accounts, | |
| 100 | + notes="Profil auto-déclaré sur l'annuaire OnlyQueb (créateurs " | |
| 101 | + "québécois sur abonnement, 18+).", | |
| 102 | + source_ids=[f"{self.source_id}:{slug}"], | |
| 103 | + )) | |
| 104 | + return creators | |
added
creaka/connectors/tiktok.py
+100 −0
@@ -0,0 +1,100 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/tiktok.py | |
| 4 | +# Desc: Connecteur ENRICHISSEMENT TikTok — profil public par créateur : | |
| 5 | +# abonnés, badge vérifié, bio, lien de bio (découverte croisée). | |
| 6 | +# Mode d'accès : Scrapfly (plateforme hostile au scraping, API | |
| 7 | +# officielle limitée aux comptes connectés) — palier 4 (§9), §15. | |
| 8 | +# ============================================================================== | |
| 9 | +"""Enrichissement TikTok, créateur par créateur. | |
| 10 | + | |
| 11 | +La page publique tiktok.com/@handle embarque un JSON d'hydratation | |
| 12 | +(__UNIVERSAL_DATA_FOR_REHYDRATION__) contenant stats.followerCount, la bio | |
| 13 | +(`signature`), le badge vérifié et le lien de bio (`bioLink`). On le lit via | |
| 14 | +Scrapfly. Lien de bio → page link-in-bio ou compte croisé (signal cross_link). | |
| 15 | +""" | |
| 16 | +from __future__ import annotations | |
| 17 | + | |
| 18 | +import json | |
| 19 | +import re | |
| 20 | + | |
| 21 | +from ..identity import account, merge_accounts | |
| 22 | +from ..normalize import parse_count, platform_from_url | |
| 23 | +from ..schema import Creator, now_iso | |
| 24 | +from .base import BaseConnector | |
| 25 | +from .linkinbio import is_supported as is_linkinbio | |
| 26 | + | |
| 27 | +PROFILE_URL = "https://www.tiktok.com/@{h}" | |
| 28 | +_STATE_RE = re.compile( | |
| 29 | + r'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>(.*?)</script>', re.S) | |
| 30 | + | |
| 31 | + | |
| 32 | +def parse_profile(html: str) -> dict | None: | |
| 33 | + """HTML de profil TikTok → dict userInfo {user:…, stats:…} (None si absent).""" | |
| 34 | + m = _STATE_RE.search(html) | |
| 35 | + if not m: | |
| 36 | + return None | |
| 37 | + try: | |
| 38 | + data = json.loads(m.group(1)) | |
| 39 | + return (data.get("__DEFAULT_SCOPE__", {}) | |
| 40 | + .get("webapp.user-detail", {}) | |
| 41 | + .get("userInfo")) or None | |
| 42 | + except Exception: | |
| 43 | + return None | |
| 44 | + | |
| 45 | + | |
| 46 | +class TikTokConnector(BaseConnector): | |
| 47 | + source_id = "tiktok-profil" | |
| 48 | + kind = "enrichment" | |
| 49 | + request_delay = 1.5 | |
| 50 | + max_profiles = 700 | |
| 51 | + | |
| 52 | + def enrich(self, creators: list[Creator]) -> list[Creator]: | |
| 53 | + enriched: list[Creator] = [] | |
| 54 | + fetched = 0 | |
| 55 | + for cr in creators: | |
| 56 | + tk = next((a for a in cr.platforms if a.platform == "tiktok"), None) | |
| 57 | + if tk is None or fetched >= self.max_profiles: | |
| 58 | + continue | |
| 59 | + try: | |
| 60 | + result = self.scrapfly(PROFILE_URL.format(h=tk.handle), | |
| 61 | + render_js=False) | |
| 62 | + except Exception: | |
| 63 | + continue | |
| 64 | + fetched += 1 | |
| 65 | + if result.get("status_code") != 200: | |
| 66 | + continue | |
| 67 | + info = parse_profile(result.get("content") or "") | |
| 68 | + if not info: | |
| 69 | + continue | |
| 70 | + user = info.get("user") or {} | |
| 71 | + stats = info.get("stats") or {} | |
| 72 | + if user.get("privateAccount"): | |
| 73 | + continue # §15 : uniquement du public | |
| 74 | + tk.followers = parse_count(stats.get("followerCount")) or tk.followers | |
| 75 | + tk.verified = bool(user.get("verified")) | |
| 76 | + tk.last_checked = now_iso() | |
| 77 | + extra = { | |
| 78 | + "following": parse_count(stats.get("followingCount")), | |
| 79 | + "likes": parse_count(stats.get("heartCount")), | |
| 80 | + "videos": parse_count(stats.get("videoCount")), | |
| 81 | + } | |
| 82 | + tk.metrics.update({k: v for k, v in extra.items() if v is not None}) | |
| 83 | + if not cr.avatar_url and user.get("avatarLarger"): | |
| 84 | + cr.avatar_url = user["avatarLarger"] | |
| 85 | + if not cr.bio and user.get("signature"): | |
| 86 | + cr.bio = user["signature"] | |
| 87 | + url = ((user.get("bioLink") or {}).get("link") or "").strip() | |
| 88 | + if url and not url.startswith("http"): | |
| 89 | + url = "https://" + url | |
| 90 | + if url: | |
| 91 | + if is_linkinbio(url) and not cr.link_in_bio_url: | |
| 92 | + cr.link_in_bio_url = url | |
| 93 | + else: | |
| 94 | + hit = platform_from_url(url) | |
| 95 | + if hit and hit[0] != "tiktok": | |
| 96 | + cr.platforms = merge_accounts(cr.platforms, [ | |
| 97 | + account(hit[0], hit[1], "cross_link", | |
| 98 | + url=url).finalize()]) | |
| 99 | + enriched.append(cr) | |
| 100 | + return enriched | |
added
creaka/connectors/twitch.py
+67 −0
@@ -0,0 +1,67 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/twitch.py | |
| 4 | +# Desc: Connecteur ENRICHISSEMENT Twitch — existence + profil par créateur | |
| 5 | +# via l'API OFFICIELLE Helix (client_credentials). Palier 2 (§9, §10). | |
| 6 | +# Sans TWITCH_CLIENT_ID/TWITCH_CLIENT_SECRET : passage sauté proprement. | |
| 7 | +# ============================================================================== | |
| 8 | +"""Enrichissement Twitch via l'API officielle Helix. | |
| 9 | + | |
| 10 | +`users?login=` confirme l'existence du compte, son type (partner/affiliate) | |
| 11 | +et sa description. (Le total d'abonnés de chaîne exige un jeton de modérateur | |
| 12 | +depuis 2023 — on ne le collecte pas plutôt que de contourner, §15.) | |
| 13 | +""" | |
| 14 | +from __future__ import annotations | |
| 15 | + | |
| 16 | +import os | |
| 17 | + | |
| 18 | +from ..schema import Creator, now_iso | |
| 19 | +from .base import BaseConnector | |
| 20 | + | |
| 21 | +TOKEN_URL = "https://id.twitch.tv/oauth2/token" | |
| 22 | +USERS_URL = "https://api.twitch.tv/helix/users" | |
| 23 | + | |
| 24 | + | |
| 25 | +class TwitchConnector(BaseConnector): | |
| 26 | + source_id = "twitch" | |
| 27 | + kind = "enrichment" | |
| 28 | + request_delay = 0.5 | |
| 29 | + max_profiles = 400 | |
| 30 | + | |
| 31 | + def _token(self, cid: str, secret: str) -> str: | |
| 32 | + resp = self.post(TOKEN_URL, data={"client_id": cid, | |
| 33 | + "client_secret": secret, | |
| 34 | + "grant_type": "client_credentials"}) | |
| 35 | + return resp.json()["access_token"] | |
| 36 | + | |
| 37 | + def enrich(self, creators: list[Creator]) -> list[Creator]: | |
| 38 | + cid = os.environ.get("TWITCH_CLIENT_ID", "") | |
| 39 | + secret = os.environ.get("TWITCH_CLIENT_SECRET", "") | |
| 40 | + if not cid or not secret: | |
| 41 | + return [] # pas de clés → on saute (jamais de contournement, §15) | |
| 42 | + token = self._token(cid, secret) | |
| 43 | + headers = {"Client-Id": cid, "Authorization": f"Bearer {token}"} | |
| 44 | + enriched: list[Creator] = [] | |
| 45 | + fetched = 0 | |
| 46 | + for cr in creators: | |
| 47 | + tw = next((a for a in cr.platforms if a.platform == "twitch"), None) | |
| 48 | + if tw is None or fetched >= self.max_profiles: | |
| 49 | + continue | |
| 50 | + try: | |
| 51 | + resp = self.get(USERS_URL, params={"login": tw.handle}, | |
| 52 | + headers=headers) | |
| 53 | + except Exception: | |
| 54 | + continue | |
| 55 | + fetched += 1 | |
| 56 | + data = resp.json().get("data") or [] | |
| 57 | + if not data: | |
| 58 | + continue | |
| 59 | + user = data[0] | |
| 60 | + tw.verified = user.get("broadcaster_type") == "partner" | |
| 61 | + tw.confidence = max(tw.confidence or 0, 0.90) | |
| 62 | + tw.signal = tw.signal or "api_officielle" | |
| 63 | + tw.last_checked = now_iso() | |
| 64 | + if not cr.bio and user.get("description"): | |
| 65 | + cr.bio = user["description"] | |
| 66 | + enriched.append(cr) | |
| 67 | + return enriched | |
added
creaka/connectors/wikidata_qc.py
+128 −0
@@ -0,0 +1,128 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/wikidata_qc.py | |
| 4 | +# Desc: Connecteur DÉCOUVERTE — créateurs québécois recensés dans Wikidata | |
| 5 | +# (personnes nées ou résidant au Québec, occupation créateur, avec | |
| 6 | +# handles sociaux curés P2003/P7085/P2397/P5797/P2002). Mode d'accès : | |
| 7 | +# API SPARQL publique de Wikidata (gratuite, conforme). Palier 3 (§9). | |
| 8 | +# ============================================================================== | |
| 9 | +"""Découverte via Wikidata : au-delà des listes médias. | |
| 10 | + | |
| 11 | +Wikidata référence les créateurs notables (humoristes, youtubeurs, vidéastes | |
| 12 | +web, streamers, podcasteurs, influenceurs) avec leurs comptes sociaux CURÉS | |
| 13 | +par la communauté → signal `base_publique` (0.85). Deux requêtes (lieu de | |
| 14 | +naissance / lieu de résidence au Québec) fusionnées. | |
| 15 | + | |
| 16 | +Prudence mineurs (§15) : l'année de naissance publique permet de lever | |
| 17 | +`is_minor` automatiquement → régime restreint appliqué par ethics.py. | |
| 18 | +""" | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +from datetime import datetime, timezone | |
| 22 | + | |
| 23 | +from ..identity import account | |
| 24 | +from ..normalize import slugify | |
| 25 | +from ..schema import Creator | |
| 26 | +from .base import BaseConnector | |
| 27 | + | |
| 28 | +SPARQL_URL = "https://query.wikidata.org/sparql" | |
| 29 | + | |
| 30 | +# occupations « créateur de contenu » (ids Wikidata) | |
| 31 | +# Q17125263 vidéaste web · Q94791573 tiktokeur · Q50279140 streamer | |
| 32 | +# Q15077007 podcasteur · Q2906862 influenceur · Q245068 humoriste | |
| 33 | +_OCCS = "wd:Q17125263 wd:Q94791573 wd:Q50279140 wd:Q15077007 wd:Q2906862 wd:Q245068" | |
| 34 | + | |
| 35 | +_OCC_NICHE = { | |
| 36 | + "Q245068": "humour", "Q50279140": "gaming", "Q15077007": "actualite-opinion", | |
| 37 | + "Q17125263": "lifestyle", "Q94791573": "lifestyle", "Q2906862": "lifestyle", | |
| 38 | +} | |
| 39 | + | |
| 40 | +_QUERY = """ | |
| 41 | +SELECT DISTINCT ?person ?personLabel ?occ ?ig ?tk ?yt ?tw ?x (YEAR(?dob) AS ?birthYear) WHERE { | |
| 42 | + VALUES ?occ { %(occs)s } | |
| 43 | + ?person wdt:P106 ?occ . | |
| 44 | + ?person wdt:%(place_prop)s ?place . | |
| 45 | + ?place wdt:P131* wd:Q176 . hint:Prior hint:gearing "forward" . | |
| 46 | + OPTIONAL { ?person wdt:P2003 ?ig } | |
| 47 | + OPTIONAL { ?person wdt:P7085 ?tk } | |
| 48 | + OPTIONAL { ?person wdt:P2397 ?yt } | |
| 49 | + OPTIONAL { ?person wdt:P5797 ?tw } | |
| 50 | + OPTIONAL { ?person wdt:P2002 ?x } | |
| 51 | + OPTIONAL { ?person wdt:P569 ?dob } | |
| 52 | + FILTER(BOUND(?ig) || BOUND(?tk) || BOUND(?yt) || BOUND(?tw) || BOUND(?x)) | |
| 53 | + SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en". } | |
| 54 | +} | |
| 55 | +LIMIT 3000 | |
| 56 | +""" | |
| 57 | + | |
| 58 | + | |
| 59 | +class WikidataQcConnector(BaseConnector): | |
| 60 | + source_id = "wikidata-qc" | |
| 61 | + kind = "discovery" | |
| 62 | + request_delay = 2.0 | |
| 63 | + timeout = 90 | |
| 64 | + | |
| 65 | + def _query(self, place_prop: str) -> list[dict]: | |
| 66 | + resp = self.get(SPARQL_URL, | |
| 67 | + params={"query": _QUERY % {"occs": _OCCS, | |
| 68 | + "place_prop": place_prop}}, | |
| 69 | + headers={"Accept": "application/sparql-results+json"}) | |
| 70 | + return resp.json().get("results", {}).get("bindings", []) | |
| 71 | + | |
| 72 | + def fetch(self) -> list[Creator]: | |
| 73 | + rows: list[dict] = [] | |
| 74 | + for prop in ("P19", "P551"): # naissance, résidence | |
| 75 | + try: | |
| 76 | + rows.extend(self._query(prop)) | |
| 77 | + except Exception: | |
| 78 | + continue # une branche en échec ne bloque pas l'autre | |
| 79 | + by_person: dict[str, dict] = {} | |
| 80 | + for r in rows: | |
| 81 | + def val(key): | |
| 82 | + return (r.get(key) or {}).get("value") | |
| 83 | + pid = val("person") | |
| 84 | + if not pid: | |
| 85 | + continue | |
| 86 | + entry = by_person.setdefault( | |
| 87 | + pid, {"name": val("personLabel"), "occs": set(), "birth": None, | |
| 88 | + "handles": {}}) | |
| 89 | + entry["occs"].add((val("occ") or "").rsplit("/", 1)[-1]) | |
| 90 | + if val("birthYear"): | |
| 91 | + entry["birth"] = int(val("birthYear")) | |
| 92 | + for key, platform in (("ig", "instagram"), ("tk", "tiktok"), | |
| 93 | + ("tw", "twitch"), ("x", "x")): | |
| 94 | + if val(key): | |
| 95 | + entry["handles"][platform] = val(key) | |
| 96 | + # P2397 = ID de chaîne YouTube (UC…) → URL directe, pas un handle @ | |
| 97 | + if val("yt"): | |
| 98 | + entry["handles"]["youtube_channel"] = val("yt") | |
| 99 | + year_now = datetime.now(timezone.utc).year | |
| 100 | + creators: list[Creator] = [] | |
| 101 | + for pid, e in by_person.items(): | |
| 102 | + name = (e["name"] or "").strip() | |
| 103 | + qid = pid.rsplit("/", 1)[-1] | |
| 104 | + if not name or name == qid: | |
| 105 | + continue # entité sans libellé humain | |
| 106 | + accounts = [] | |
| 107 | + for platform, handle in e["handles"].items(): | |
| 108 | + if platform == "youtube_channel": | |
| 109 | + accounts.append(account( | |
| 110 | + "youtube", handle, "base_publique", | |
| 111 | + url=f"https://www.youtube.com/channel/{handle}")) | |
| 112 | + else: | |
| 113 | + accounts.append(account(platform, handle, "base_publique")) | |
| 114 | + if not accounts: | |
| 115 | + continue | |
| 116 | + is_minor = bool(e["birth"] and (year_now - e["birth"]) < 18) | |
| 117 | + niches = sorted({_OCC_NICHE.get(o, "autre") for o in e["occs"]}) or ["autre"] | |
| 118 | + creators.append(Creator( | |
| 119 | + source=self.source_id, | |
| 120 | + external_id=qid, | |
| 121 | + display_name=name, | |
| 122 | + niches=niches, | |
| 123 | + is_minor=is_minor, | |
| 124 | + platforms=accounts, | |
| 125 | + source_ids=[f"{self.source_id}:{qid}"], | |
| 126 | + )) | |
| 127 | + # tri stable pour des passages reproductibles | |
| 128 | + return sorted(creators, key=lambda c: slugify(c.display_name)) | |
added
creaka/connectors/youtube.py
+89 −0
@@ -0,0 +1,89 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/youtube.py | |
| 4 | +# Desc: Connecteur ENRICHISSEMENT YouTube — abonnés + badge par créateur. | |
| 5 | +# Mode d'accès : YouTube Data API v3 (OFFICIELLE, privilégiée §10) si | |
| 6 | +# YOUTUBE_API_KEY est définie ; sinon repli page publique /@handle | |
| 7 | +# (requêtes directes polies). Palier 2 du catalogue (§9). | |
| 8 | +# ============================================================================== | |
| 9 | +"""Enrichissement YouTube, créateur par créateur. | |
| 10 | + | |
| 11 | +Voie privilégiée : l'API officielle `channels?forHandle=` (fiable, conforme, | |
| 12 | +quota 10 000 unités/jour largement suffisant pour la cadence §14). Sans clé, | |
| 13 | +repli léger : la page publique youtube.com/@handle expose le nombre d'abonnés | |
| 14 | +dans ses métadonnées. Signal : `api_officielle` (0.95) quand confirmé par | |
| 15 | +l'API — le handle interrogé est celui déjà rattaché. | |
| 16 | +""" | |
| 17 | +from __future__ import annotations | |
| 18 | + | |
| 19 | +import os | |
| 20 | +import re | |
| 21 | + | |
| 22 | +from ..normalize import parse_count | |
| 23 | +from ..schema import Creator, now_iso | |
| 24 | +from .base import BaseConnector | |
| 25 | + | |
| 26 | +API_URL = ("https://www.googleapis.com/youtube/v3/channels" | |
| 27 | + "?part=statistics,snippet&forHandle={h}&key={key}") | |
| 28 | +PAGE_URL = "https://www.youtube.com/@{h}" | |
| 29 | +_SUBS_RES = ( | |
| 30 | + re.compile(r'"subscriberCountText"[^}]*?"(?:simpleText|content)"\s*:\s*"([^"]+)"'), | |
| 31 | + re.compile(r'"subscriberCount"\s*:\s*"?(\d+)"?'), | |
| 32 | + re.compile(r'"metadataParts".{0,200}?"content"\s*:\s*"([\d.,\s]+[kKmM]?\s*' | |
| 33 | + r'(?:subscribers|abonn[ée]s))"'), | |
| 34 | +) | |
| 35 | + | |
| 36 | + | |
| 37 | +class YouTubeConnector(BaseConnector): | |
| 38 | + source_id = "youtube" | |
| 39 | + kind = "enrichment" | |
| 40 | + request_delay = 1.0 | |
| 41 | + max_profiles = 1500 | |
| 42 | + | |
| 43 | + def _via_api(self, handle: str, key: str) -> dict | None: | |
| 44 | + resp = self.get(API_URL.format(h=handle, key=key)) | |
| 45 | + items = resp.json().get("items") or [] | |
| 46 | + if not items: | |
| 47 | + return None | |
| 48 | + stats = items[0].get("statistics") or {} | |
| 49 | + return {"followers": parse_count(stats.get("subscriberCount")), | |
| 50 | + "views": parse_count(stats.get("viewCount")), | |
| 51 | + "videos": parse_count(stats.get("videoCount")), | |
| 52 | + "confirmed": True} | |
| 53 | + | |
| 54 | + def _via_page(self, handle: str) -> dict | None: | |
| 55 | + resp = self.get(PAGE_URL.format(h=handle), | |
| 56 | + headers={"Accept-Language": "en-CA,en;q=0.8"}) | |
| 57 | + html = resp.text | |
| 58 | + for rx in _SUBS_RES: | |
| 59 | + m = rx.search(html) | |
| 60 | + if m: | |
| 61 | + return {"followers": parse_count(m.group(1)), "confirmed": False} | |
| 62 | + return None | |
| 63 | + | |
| 64 | + def enrich(self, creators: list[Creator]) -> list[Creator]: | |
| 65 | + key = os.environ.get("YOUTUBE_API_KEY", "") | |
| 66 | + enriched: list[Creator] = [] | |
| 67 | + fetched = 0 | |
| 68 | + for cr in creators: | |
| 69 | + yt = next((a for a in cr.platforms if a.platform == "youtube"), None) | |
| 70 | + if yt is None or fetched >= self.max_profiles: | |
| 71 | + continue | |
| 72 | + try: | |
| 73 | + info = self._via_api(yt.handle, key) if key \ | |
| 74 | + else self._via_page(yt.handle) | |
| 75 | + except Exception: | |
| 76 | + continue | |
| 77 | + fetched += 1 | |
| 78 | + if not info or info.get("followers") is None: | |
| 79 | + continue | |
| 80 | + yt.followers = info["followers"] | |
| 81 | + yt.last_checked = now_iso() | |
| 82 | + extra = {k: info.get(k) for k in ("views", "videos")} | |
| 83 | + yt.metrics.update({k: v for k, v in extra.items() if v is not None}) | |
| 84 | + if info.get("confirmed"): | |
| 85 | + # existence confirmée par l'API officielle → signal renforcé | |
| 86 | + yt.confidence = max(yt.confidence or 0, 0.90) | |
| 87 | + yt.signal = yt.signal or "api_officielle" | |
| 88 | + enriched.append(cr) | |
| 89 | + return enriched | |
added
creaka/connectors/youtube_recherche.py
+281 −0
@@ -0,0 +1,281 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/connectors/youtube_recherche.py | |
| 4 | +# Desc: Connecteur DÉCOUVERTE — chaînes YouTube québécoises via la recherche | |
| 5 | +# publique (filtre « chaînes »), requêtes thématiques × marqueurs QC. | |
| 6 | +# Mode d'accès : requêtes directes (HTML public, ytInitialData). | |
| 7 | +# Palier 3 (§9). Filtre conservateur : marqueur québécois OBLIGATOIRE | |
| 8 | +# dans le titre ou la description de la chaîne. | |
| 9 | +# ============================================================================== | |
| 10 | +"""Découverte massive de chaînes YouTube québécoises. | |
| 11 | + | |
| 12 | +La page de résultats (filtre chaînes, sp=EgIQAg==) embarque un JSON | |
| 13 | +ytInitialData avec, pour chaque chaîne : handle, nom, nb d'abonnés et extrait | |
| 14 | +de description. On ne crée une fiche QUE si un marqueur québécois figure dans | |
| 15 | +le titre/la description (jamais d'inférence, §7/§15). Le compte YouTube est le | |
| 16 | +compte SOURCE de la fiche → signal profil_source (0.98) ; l'enrichissement | |
| 17 | +(liens de bio, link-in-bio) rattache ensuite ses autres plateformes. | |
| 18 | +""" | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import json | |
| 22 | +import re | |
| 23 | + | |
| 24 | +from ..identity import account | |
| 25 | +from ..normalize import parse_count, slugify, strip_accents | |
| 26 | +from ..schema import Creator | |
| 27 | +from .base import BaseConnector | |
| 28 | + | |
| 29 | +SEARCH_URL = "https://www.youtube.com/results" | |
| 30 | +CHANNEL_FILTER = "EgIQAg%3D%3D" # sp= filtre « chaînes » | |
| 31 | +_YTID_RE = re.compile(r"var ytInitialData = (\{.*?\});</script>", re.S) | |
| 32 | + | |
| 33 | +# marqueurs québécois (comparés SANS accents, en minuscules) | |
| 34 | +_QC_MARKERS = re.compile( | |
| 35 | + r"queb|montreal|\bmtl\b|saguenay|gaspesie|mauricie|estrie|outaouais|" | |
| 36 | + r"abitibi|levis|sherbrooke|trois-rivieres|gatineau|\blaval\b|longueuil|" | |
| 37 | + r"rimouski|chicoutimi|drummondville|granby|beauce|charlevoix|lanaudiere|" | |
| 38 | + r"laurentides|monteregie|temiscamingue|joliette|victoriaville|" | |
| 39 | + r"\bfleuve\b|rive-sud|rive-nord|ville de quebec") | |
| 40 | + | |
| 41 | +# requêtes thématiques → niche par défaut de la fiche | |
| 42 | +QUERIES: list[tuple[str, str]] = [ | |
| 43 | + ("vlog québécois", "lifestyle"), ("youtubeur québécois", "lifestyle"), | |
| 44 | + ("humour québécois", "humour"), ("humoriste québécois", "humour"), | |
| 45 | + ("cuisine québécoise", "cuisine"), ("recettes québec", "cuisine"), | |
| 46 | + ("gaming québécois", "gaming"), ("gamer québec", "gaming"), | |
| 47 | + ("chasse québec", "plein-air"), ("pêche québec", "plein-air"), | |
| 48 | + ("plein air québec", "plein-air"), ("camping québec", "plein-air"), | |
| 49 | + ("musique québécoise", "musique"), ("rap québécois", "musique"), | |
| 50 | + ("maquillage québec", "beaute"), ("beauté québécoise", "beaute"), | |
| 51 | + ("mode québécoise", "mode"), ("fitness québec", "sport-fitness"), | |
| 52 | + ("entraînement québécois", "sport-fitness"), ("hockey québec", "sport-fitness"), | |
| 53 | + ("famille québécoise vlog", "famille-parentalite"), | |
| 54 | + ("maman québécoise", "famille-parentalite"), | |
| 55 | + ("voyage québécois", "voyage"), ("finance québec", "finance-affaires"), | |
| 56 | + ("immobilier québec", "finance-affaires"), ("tech québec", "tech"), | |
| 57 | + ("balado québécois", "actualite-opinion"), ("podcast québécois", "actualite-opinion"), | |
| 58 | + ("éducation québec", "education"), ("science québec", "education"), | |
| 59 | + ("art québécois", "arts"), ("danse québec", "danse"), | |
| 60 | + ("resto montréal", "bouffe-resto"), ("bouffe québec", "bouffe-resto"), | |
| 61 | + ("vlog montréal", "lifestyle"), ("vlog ville de québec", "lifestyle"), | |
| 62 | + ("agriculture québec", "autre"), ("auto québec", "autre"), | |
| 63 | + ("santé bien-être québec", "sante-mieux-etre"), | |
| 64 | + ("actualité québécoise", "actualite-opinion"), | |
| 65 | + # villes × vlog/scène locale | |
| 66 | + ("vlog sherbrooke", "lifestyle"), ("vlog gatineau", "lifestyle"), | |
| 67 | + ("vlog saguenay", "lifestyle"), ("vlog trois-rivières", "lifestyle"), | |
| 68 | + ("vlog gaspésie", "voyage"), ("vlog rimouski", "lifestyle"), | |
| 69 | + ("vlog abitibi", "lifestyle"), ("vlog lévis", "lifestyle"), | |
| 70 | + ("vlog laval", "lifestyle"), ("vlog longueuil", "lifestyle"), | |
| 71 | + ("vlog drummondville", "lifestyle"), ("vlog granby", "lifestyle"), | |
| 72 | + # gaming & streaming | |
| 73 | + ("minecraft québécois", "gaming"), ("fortnite québécois", "gaming"), | |
| 74 | + ("streamer québécois", "gaming"), ("twitch québécois", "gaming"), | |
| 75 | + ("jeux vidéo québec", "gaming"), ("rétro gaming québec", "gaming"), | |
| 76 | + # niches ciblées | |
| 77 | + ("asmr québécois", "lifestyle"), ("vanlife québec", "voyage"), | |
| 78 | + ("ski québec", "plein-air"), ("vélo québec", "sport-fitness"), | |
| 79 | + ("course à pied québec", "sport-fitness"), ("musculation québec", "sport-fitness"), | |
| 80 | + ("nutrition québec", "sante-mieux-etre"), ("yoga québec", "sante-mieux-etre"), | |
| 81 | + ("rénovation québec", "autre"), ("menuiserie québec", "autre"), | |
| 82 | + ("jardinage québec", "plein-air"), ("moto québec", "autre"), | |
| 83 | + ("auto détail québec", "autre"), ("mécanique québec", "autre"), | |
| 84 | + ("crypto québec", "finance-affaires"), ("bourse québec", "finance-affaires"), | |
| 85 | + ("entrepreneur québécois", "finance-affaires"), | |
| 86 | + ("développeur québécois", "tech"), ("programmation québec", "tech"), | |
| 87 | + ("photographie québec", "arts"), ("dessin québécois", "arts"), | |
| 88 | + ("couture québec", "arts"), ("tricot québec", "arts"), | |
| 89 | + ("chasse orignal québec", "plein-air"), ("motoneige québec", "plein-air"), | |
| 90 | + ("quatre-roues québec", "plein-air"), ("survie en forêt québec", "plein-air"), | |
| 91 | + ("astronomie québec", "education"), ("histoire du québec", "education"), | |
| 92 | + ("hockey canadiens de montréal", "sport-fitness"), | |
| 93 | + ("pêche mouche québec", "plein-air"), ("kayak québec", "plein-air"), | |
| 94 | + ("randonnée québec", "plein-air"), ("microbrasserie québec", "bouffe-resto"), | |
| 95 | + ("bbq québec", "cuisine"), ("pâtisserie québec", "cuisine"), | |
| 96 | + ("végane québec", "cuisine"), ("chef québécois", "cuisine"), | |
| 97 | + ("critique resto montréal", "bouffe-resto"), | |
| 98 | + ("maquilleuse québécoise", "beaute"), ("coiffure québec", "beaute"), | |
| 99 | + ("soins de peau québec", "beaute"), ("friperie montréal", "mode"), | |
| 100 | + ("streetwear montréal", "mode"), ("chanteur québécois", "musique"), | |
| 101 | + ("chanteuse québécoise", "musique"), ("guitare québec", "musique"), | |
| 102 | + ("dj québec", "musique"), ("country québécois", "musique"), | |
| 103 | + ("télé-réalité québec", "lifestyle"), ("occupation double", "lifestyle"), | |
| 104 | + ("humoriste relève québec", "humour"), ("stand up québec", "humour"), | |
| 105 | + ("améliorer son français québec", "education"), | |
| 106 | + ("prof québécois", "education"), ("garderie maman québec", "famille-parentalite"), | |
| 107 | + ("grossesse québec", "famille-parentalite"), | |
| 108 | + ("astuces parents québec", "famille-parentalite"), | |
| 109 | + ("spiritualité québec", "sante-mieux-etre"), | |
| 110 | + ("astrologie québec", "autre"), ("paranormal québec", "autre"), | |
| 111 | + ("urbex québec", "autre"), ("faits divers québec", "actualite-opinion"), | |
| 112 | + ("politique québécoise", "actualite-opinion"), | |
| 113 | + ("nouvelles québec", "actualite-opinion"), | |
| 114 | +] | |
| 115 | + | |
| 116 | +# --- longue traîne : villes/MRC × créneaux (nano/micro créateurs locaux) -------- | |
| 117 | +# On ne vise PAS que les mégastars : chaque ville a ses vloggeurs, gamers, | |
| 118 | +# chasseurs, cuisiniers… Le filtre _QC_MARKERS garde la prudence d'attribution. | |
| 119 | +_TOWNS = [ | |
| 120 | + "montréal", "ville de québec", "laval", "gatineau", "longueuil", | |
| 121 | + "sherbrooke", "lévis", "saguenay", "trois-rivières", "terrebonne", | |
| 122 | + "brossard", "repentigny", "drummondville", "saint-jérôme", "granby", | |
| 123 | + "blainville", "saint-hyacinthe", "shawinigan", "dollard-des-ormeaux", | |
| 124 | + "rimouski", "châteauguay", "saint-jean-sur-richelieu", "mascouche", | |
| 125 | + "joliette", "victoriaville", "salaberry-de-valleyfield", "rouyn-noranda", | |
| 126 | + "chicoutimi", "alma", "sept-îles", "baie-comeau", "val-d'or", "matane", | |
| 127 | + "rivière-du-loup", "sorel-tracy", "thetford mines", "magog", "cowansville", | |
| 128 | + "la tuque", "mont-laurier", "mont-tremblant", "sainte-agathe", | |
| 129 | + "saint-georges beauce", "montmagny", "la malbaie", "baie-saint-paul", | |
| 130 | + "gaspé", "chandler", "amqui", "dolbeau-mistassini", "roberval", | |
| 131 | + "chibougamau", "lachute", "varennes", "chambly", "boucherville", | |
| 132 | +] | |
| 133 | +_TOWN_THEMES = [("vlog", "lifestyle"), ("youtubeur", "lifestyle"), | |
| 134 | + ("gaming", "gaming"), ("chasse pêche", "plein-air"), | |
| 135 | + ("humour", "humour")] | |
| 136 | +for _t in _TOWNS: | |
| 137 | + for _th, _n in _TOWN_THEMES: | |
| 138 | + QUERIES.append((f"{_th} {_t}", _n)) | |
| 139 | + | |
| 140 | +# --- longue traîne : passe-temps & métiers × marqueur QC ------------------------ | |
| 141 | +_HOBBIES = [ | |
| 142 | + ("skateboard", "sport-fitness"), ("snowboard", "plein-air"), | |
| 143 | + ("pickleball", "sport-fitness"), ("golf", "sport-fitness"), | |
| 144 | + ("baseball", "sport-fitness"), ("soccer", "sport-fitness"), | |
| 145 | + ("basketball", "sport-fitness"), ("boxe", "sport-fitness"), | |
| 146 | + ("mma", "sport-fitness"), ("crossfit", "sport-fitness"), | |
| 147 | + ("échecs", "gaming"), ("donjons et dragons", "gaming"), | |
| 148 | + ("cartes pokémon", "gaming"), ("warhammer", "gaming"), | |
| 149 | + ("speedrun", "gaming"), ("simracing", "gaming"), | |
| 150 | + ("lego", "autre"), ("maquette", "arts"), ("bois", "arts"), | |
| 151 | + ("forge", "arts"), ("céramique", "arts"), ("peinture", "arts"), | |
| 152 | + ("aquarelle", "arts"), ("calligraphie", "arts"), | |
| 153 | + ("astrophotographie", "education"), ("ornithologie", "plein-air"), | |
| 154 | + ("apiculture", "plein-air"), ("permaculture", "plein-air"), | |
| 155 | + ("homesteading", "plein-air"), ("érablière", "plein-air"), | |
| 156 | + ("acériculture", "plein-air"), ("motocross", "autre"), | |
| 157 | + ("vtt", "plein-air"), ("side-by-side", "plein-air"), | |
| 158 | + ("camion", "autre"), ("remorquage", "autre"), ("soudure", "autre"), | |
| 159 | + ("électricien", "autre"), ("plombier", "autre"), ("excavation", "autre"), | |
| 160 | + ("déneigement", "autre"), ("paysagement", "autre"), | |
| 161 | + ("courtier immobilier", "finance-affaires"), ("comptable", "finance-affaires"), | |
| 162 | + ("avocat", "education"), ("infirmière", "sante-mieux-etre"), | |
| 163 | + ("massothérapie", "sante-mieux-etre"), ("ostéopathie", "sante-mieux-etre"), | |
| 164 | + ("naturopathie", "sante-mieux-etre"), ("psychologie", "sante-mieux-etre"), | |
| 165 | + ("méditation", "sante-mieux-etre"), ("tarot", "autre"), | |
| 166 | + ("généalogie", "education"), ("philosophie", "education"), | |
| 167 | + ("booktube", "education"), ("lecture", "education"), | |
| 168 | + ("cinéma", "actualite-opinion"), ("séries télé", "actualite-opinion"), | |
| 169 | + ("anime", "gaming"), ("manga", "gaming"), ("cosplay", "arts"), | |
| 170 | + ("drag", "arts"), ("magie", "arts"), ("cirque", "arts"), | |
| 171 | + ("violon", "musique"), ("piano", "musique"), ("batterie", "musique"), | |
| 172 | + ("chorale", "musique"), ("trad", "musique"), ("métal", "musique"), | |
| 173 | + ("punk", "musique"), ("folk", "musique"), | |
| 174 | + ("fromage", "bouffe-resto"), ("vin", "bouffe-resto"), | |
| 175 | + ("café", "bouffe-resto"), ("boulangerie", "cuisine"), | |
| 176 | + ("cabane à sucre", "bouffe-resto"), ("poutine", "bouffe-resto"), | |
| 177 | + ("food truck", "bouffe-resto"), ("mixologie", "bouffe-resto"), | |
| 178 | + ("couture", "arts"), ("crochet", "arts"), ("broderie", "arts"), | |
| 179 | + ("thrift", "mode"), ("sneakers", "mode"), ("barbier", "beaute"), | |
| 180 | + ("ongles", "beaute"), ("tatouage", "arts"), ("esthétique", "beaute"), | |
| 181 | + ("organisation maison", "lifestyle"), ("minimalisme", "lifestyle"), | |
| 182 | + ("van aménagé", "voyage"), ("chalet", "lifestyle"), | |
| 183 | + ("astuces budget", "finance-affaires"), ("coupons", "finance-affaires"), | |
| 184 | + ("frugal", "finance-affaires"), ("bourse dividendes", "finance-affaires"), | |
| 185 | + ("intelligence artificielle", "tech"), ("cybersécurité", "tech"), | |
| 186 | + ("impression 3d", "tech"), ("domotique", "tech"), ("drones", "tech"), | |
| 187 | +] | |
| 188 | +for _h, _n in _HOBBIES: | |
| 189 | + QUERIES.append((f"{_h} québec", _n)) | |
| 190 | + QUERIES.append((f"{_h} québécois", _n)) | |
| 191 | + | |
| 192 | + | |
| 193 | +def _walk_channels(node, out: list) -> None: | |
| 194 | + if isinstance(node, dict): | |
| 195 | + if "channelRenderer" in node: | |
| 196 | + out.append(node["channelRenderer"]) | |
| 197 | + for v in node.values(): | |
| 198 | + _walk_channels(v, out) | |
| 199 | + elif isinstance(node, list): | |
| 200 | + for item in node: | |
| 201 | + _walk_channels(item, out) | |
| 202 | + | |
| 203 | + | |
| 204 | +def parse_channels(html: str) -> list[dict]: | |
| 205 | + """HTML de résultats → [{handle, channel_id, name, subs, description}].""" | |
| 206 | + m = _YTID_RE.search(html) | |
| 207 | + if not m: | |
| 208 | + return [] | |
| 209 | + try: | |
| 210 | + data = json.loads(m.group(1)) | |
| 211 | + except Exception: | |
| 212 | + return [] | |
| 213 | + renderers: list[dict] = [] | |
| 214 | + _walk_channels(data, renderers) | |
| 215 | + channels = [] | |
| 216 | + for r in renderers: | |
| 217 | + try: | |
| 218 | + handle = ((r.get("navigationEndpoint") or {}).get("browseEndpoint") | |
| 219 | + or {}).get("canonicalBaseUrl", "").lstrip("/") | |
| 220 | + name = ((r.get("title") or {}).get("simpleText")) or "" | |
| 221 | + desc = "".join(run.get("text", "") for run in | |
| 222 | + ((r.get("descriptionSnippet") or {}).get("runs") or [])) | |
| 223 | + subs_txt = ((r.get("videoCountText") or {}).get("simpleText")) or \ | |
| 224 | + ((r.get("subscriberCountText") or {}).get("simpleText")) or "" | |
| 225 | + channels.append({"handle": handle, "channel_id": r.get("channelId", ""), | |
| 226 | + "name": name, "subs": parse_count(subs_txt), | |
| 227 | + "description": desc}) | |
| 228 | + except Exception: | |
| 229 | + continue | |
| 230 | + return channels | |
| 231 | + | |
| 232 | + | |
| 233 | +def is_quebec(text: str) -> bool: | |
| 234 | + return bool(_QC_MARKERS.search(strip_accents(text or "").lower())) | |
| 235 | + | |
| 236 | + | |
| 237 | +class YouTubeRechercheConnector(BaseConnector): | |
| 238 | + source_id = "youtube-recherche" | |
| 239 | + kind = "discovery" | |
| 240 | + request_delay = 1.2 | |
| 241 | + max_queries = 800 | |
| 242 | + | |
| 243 | + def fetch(self) -> list[Creator]: | |
| 244 | + creators: list[Creator] = [] | |
| 245 | + seen: set[str] = set() | |
| 246 | + for query, niche in QUERIES[:self.max_queries]: | |
| 247 | + try: | |
| 248 | + html = self.get( | |
| 249 | + SEARCH_URL, | |
| 250 | + params={"search_query": query, "sp": "EgIQAg=="}, | |
| 251 | + headers={"Accept-Language": "fr-CA,fr;q=0.9"}).text | |
| 252 | + except Exception: | |
| 253 | + continue | |
| 254 | + for ch in parse_channels(html): | |
| 255 | + handle = ch["handle"].lstrip("@") | |
| 256 | + key = handle or ch["channel_id"] | |
| 257 | + if not key or key in seen: | |
| 258 | + continue | |
| 259 | + # filtre conservateur : marqueur QC requis dans titre+description | |
| 260 | + if not is_quebec(f"{ch['name']} {ch['description']}"): | |
| 261 | + continue | |
| 262 | + seen.add(key) | |
| 263 | + if handle: | |
| 264 | + acc = account("youtube", handle, "profil_source", | |
| 265 | + followers=ch["subs"]) | |
| 266 | + else: | |
| 267 | + acc = account("youtube", ch["channel_id"], "profil_source", | |
| 268 | + followers=ch["subs"], | |
| 269 | + url="https://www.youtube.com/channel/" | |
| 270 | + + ch["channel_id"]) | |
| 271 | + creators.append(Creator( | |
| 272 | + source=self.source_id, | |
| 273 | + external_id=slugify(ch["name"]), | |
| 274 | + display_name=ch["name"], | |
| 275 | + bio=ch["description"][:1200], | |
| 276 | + niches=[niche], | |
| 277 | + creator_type="youtubeur", | |
| 278 | + platforms=[acc], | |
| 279 | + source_ids=[f"{self.source_id}:{key}"], | |
| 280 | + )) | |
| 281 | + return creators | |
added
creaka/db.py
+362 −0
@@ -0,0 +1,362 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/db.py | |
| 4 | +# Desc: Stockage SQLite — fiches créateurs canoniques, comptes rattachés, | |
| 5 | +# journal des synchronisations (fraîcheur §13-14, opt-out §15) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Base de données Créa-Ka (SQLite, WAL). | |
| 8 | + | |
| 9 | +- `creators` : une ligne = une fiche canonique (document JSON + colonnes | |
| 10 | + indexées pour la recherche). | |
| 11 | +- `accounts` : un compte (plateforme+handle) pointe vers SA fiche — c'est | |
| 12 | + l'index qui permet la déduplication inter-sources (§12.2). | |
| 13 | +- `sync_log` : statistiques par passage de connecteur (§16 logging structuré). | |
| 14 | + | |
| 15 | +Politique de grâce (§13) : on ne supprime jamais brutalement — `last_seen` | |
| 16 | +horodate chaque passage ; un créateur disparu passe `inactive`, un opt-out | |
| 17 | +passe `opted_out` (masqué, jamais ré-agrégé). | |
| 18 | +""" | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import json | |
| 22 | +import sqlite3 | |
| 23 | +from dataclasses import asdict | |
| 24 | +from pathlib import Path | |
| 25 | + | |
| 26 | +from . import ethics | |
| 27 | +from .dedup import merge_creators, should_merge | |
| 28 | +from .identity import STRONG_THRESHOLD, needs_review | |
| 29 | +from .normalize import slugify | |
| 30 | +from .schema import Creator, PlatformAccount, now_iso | |
| 31 | + | |
| 32 | +ROOT = Path(__file__).resolve().parent.parent | |
| 33 | +DB_PATH = ROOT / "data" / "creaka.db" | |
| 34 | + | |
| 35 | +_SCHEMA = """ | |
| 36 | +CREATE TABLE IF NOT EXISTS creators ( | |
| 37 | + id TEXT PRIMARY KEY, | |
| 38 | + display_name TEXT NOT NULL, | |
| 39 | + status TEXT NOT NULL DEFAULT 'active', | |
| 40 | + is_minor INTEGER NOT NULL DEFAULT 0, | |
| 41 | + region TEXT, city TEXT, | |
| 42 | + niches TEXT, languages TEXT, | |
| 43 | + creator_type TEXT, primary_platform TEXT, | |
| 44 | + audience_tier TEXT, total_reach INTEGER, | |
| 45 | + bio TEXT, | |
| 46 | + first_seen TEXT, last_seen TEXT, updated_at TEXT, | |
| 47 | + doc TEXT NOT NULL | |
| 48 | +); | |
| 49 | +CREATE TABLE IF NOT EXISTS accounts ( | |
| 50 | + platform TEXT NOT NULL, | |
| 51 | + handle TEXT NOT NULL, | |
| 52 | + creator_id TEXT NOT NULL REFERENCES creators(id) ON DELETE CASCADE, | |
| 53 | + url TEXT NOT NULL, | |
| 54 | + followers INTEGER, | |
| 55 | + verified INTEGER, | |
| 56 | + confidence REAL NOT NULL, | |
| 57 | + signal TEXT, | |
| 58 | + needs_review INTEGER NOT NULL DEFAULT 0, | |
| 59 | + last_checked TEXT, | |
| 60 | + metrics TEXT, | |
| 61 | + PRIMARY KEY (platform, handle) | |
| 62 | +); | |
| 63 | +CREATE INDEX IF NOT EXISTS idx_accounts_creator ON accounts(creator_id); | |
| 64 | +CREATE INDEX IF NOT EXISTS idx_creators_status ON creators(status); | |
| 65 | +CREATE INDEX IF NOT EXISTS idx_creators_tier ON creators(audience_tier); | |
| 66 | +CREATE TABLE IF NOT EXISTS sync_log ( | |
| 67 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 68 | + ts TEXT NOT NULL, | |
| 69 | + source TEXT NOT NULL, | |
| 70 | + creators INTEGER, accounts INTEGER, avg_confidence REAL, | |
| 71 | + added INTEGER, updated INTEGER, errors INTEGER, seconds REAL, | |
| 72 | + alert TEXT | |
| 73 | +); | |
| 74 | +""" | |
| 75 | + | |
| 76 | + | |
| 77 | +def connect(path: Path | str = DB_PATH) -> sqlite3.Connection: | |
| 78 | + Path(path).parent.mkdir(parents=True, exist_ok=True) | |
| 79 | + con = sqlite3.connect(path, check_same_thread=False) | |
| 80 | + con.row_factory = sqlite3.Row | |
| 81 | + con.execute("PRAGMA journal_mode=WAL") | |
| 82 | + con.execute("PRAGMA foreign_keys=ON") | |
| 83 | + con.executescript(_SCHEMA) | |
| 84 | + # migrations légères (bases existantes) | |
| 85 | + try: | |
| 86 | + con.execute("ALTER TABLE accounts ADD COLUMN metrics TEXT") | |
| 87 | + except sqlite3.OperationalError: | |
| 88 | + pass | |
| 89 | + return con | |
| 90 | + | |
| 91 | + | |
| 92 | +# --- (dé)sérialisation ---------------------------------------------------------- | |
| 93 | + | |
| 94 | +def creator_to_doc(cr: Creator) -> str: | |
| 95 | + return json.dumps(asdict(cr), ensure_ascii=False) | |
| 96 | + | |
| 97 | + | |
| 98 | +def creator_from_row(row: sqlite3.Row) -> Creator: | |
| 99 | + doc = json.loads(row["doc"]) | |
| 100 | + doc["platforms"] = [PlatformAccount(**a) for a in doc.get("platforms", [])] | |
| 101 | + cr = Creator(**doc) | |
| 102 | + return cr | |
| 103 | + | |
| 104 | + | |
| 105 | +def _public_dict(row: sqlite3.Row, accounts: list[sqlite3.Row]) -> dict: | |
| 106 | + """Fiche publique (API) : comptes « à vérifier » EXCLUS (§12.1).""" | |
| 107 | + doc = json.loads(row["doc"]) | |
| 108 | + doc["id"] = row["id"] | |
| 109 | + doc["first_seen"] = row["first_seen"] | |
| 110 | + doc["last_seen"] = row["last_seen"] | |
| 111 | + doc["updated_at"] = row["updated_at"] | |
| 112 | + doc["platforms"] = [ | |
| 113 | + {"platform": a["platform"], "handle": a["handle"], "url": a["url"], | |
| 114 | + "followers": a["followers"], | |
| 115 | + "verified": bool(a["verified"]) if a["verified"] is not None else None, | |
| 116 | + "confidence": a["confidence"], "last_checked": a["last_checked"], | |
| 117 | + "metrics": json.loads(a["metrics"]) if a["metrics"] else {}} | |
| 118 | + for a in accounts if not a["needs_review"]] | |
| 119 | + # champs internes / sensibles jamais exposés tels quels | |
| 120 | + doc.pop("source", None) | |
| 121 | + doc.pop("external_id", None) | |
| 122 | + doc.pop("legal_name", None) | |
| 123 | + return doc | |
| 124 | + | |
| 125 | + | |
| 126 | +# --- écriture ------------------------------------------------------------------- | |
| 127 | + | |
| 128 | +def _unique_id(con: sqlite3.Connection, base: str) -> str: | |
| 129 | + cid, n = base, 2 | |
| 130 | + while con.execute("SELECT 1 FROM creators WHERE id=?", (cid,)).fetchone(): | |
| 131 | + cid = f"{base}-{n}" | |
| 132 | + n += 1 | |
| 133 | + return cid | |
| 134 | + | |
| 135 | + | |
| 136 | +def _write_creator(con: sqlite3.Connection, cid: str, cr: Creator, | |
| 137 | + first_seen: str, ts: str) -> None: | |
| 138 | + con.execute( | |
| 139 | + """INSERT INTO creators (id, display_name, status, is_minor, region, city, | |
| 140 | + niches, languages, creator_type, primary_platform, audience_tier, | |
| 141 | + total_reach, bio, first_seen, last_seen, updated_at, doc) | |
| 142 | + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) | |
| 143 | + ON CONFLICT(id) DO UPDATE SET | |
| 144 | + display_name=excluded.display_name, status=excluded.status, | |
| 145 | + is_minor=excluded.is_minor, region=excluded.region, city=excluded.city, | |
| 146 | + niches=excluded.niches, languages=excluded.languages, | |
| 147 | + creator_type=excluded.creator_type, | |
| 148 | + primary_platform=excluded.primary_platform, | |
| 149 | + audience_tier=excluded.audience_tier, total_reach=excluded.total_reach, | |
| 150 | + bio=excluded.bio, last_seen=excluded.last_seen, | |
| 151 | + updated_at=excluded.updated_at, doc=excluded.doc""", | |
| 152 | + (cid, cr.display_name, cr.status, int(cr.is_minor), cr.region, cr.city, | |
| 153 | + ",".join(cr.niches), ",".join(cr.languages), cr.creator_type, | |
| 154 | + cr.primary_platform, cr.audience_tier, cr.total_reach, cr.bio, | |
| 155 | + first_seen, ts, ts, creator_to_doc(cr))) | |
| 156 | + con.execute("DELETE FROM accounts WHERE creator_id=?", (cid,)) | |
| 157 | + for acc in cr.platforms: | |
| 158 | + con.execute( | |
| 159 | + """INSERT INTO accounts (platform, handle, creator_id, url, followers, | |
| 160 | + verified, confidence, signal, needs_review, last_checked, metrics) | |
| 161 | + VALUES (?,?,?,?,?,?,?,?,?,?,?) | |
| 162 | + ON CONFLICT(platform, handle) DO UPDATE SET | |
| 163 | + creator_id=excluded.creator_id, url=excluded.url, | |
| 164 | + followers=excluded.followers, verified=excluded.verified, | |
| 165 | + confidence=excluded.confidence, signal=excluded.signal, | |
| 166 | + needs_review=excluded.needs_review, | |
| 167 | + last_checked=excluded.last_checked, metrics=excluded.metrics""", | |
| 168 | + (acc.platform, acc.handle, cid, acc.url, acc.followers, | |
| 169 | + None if acc.verified is None else int(acc.verified), | |
| 170 | + acc.confidence, acc.signal, int(needs_review(acc)), acc.last_checked, | |
| 171 | + json.dumps(acc.metrics, ensure_ascii=False) if acc.metrics else None)) | |
| 172 | + | |
| 173 | + | |
| 174 | +def _find_existing(con: sqlite3.Connection, cr: Creator) -> str | None: | |
| 175 | + """Fiche existante partageant un compte FORT avec la fiche entrante (§12.2).""" | |
| 176 | + for acc in cr.platforms: | |
| 177 | + if (acc.confidence or 0) < STRONG_THRESHOLD: | |
| 178 | + continue | |
| 179 | + row = con.execute( | |
| 180 | + "SELECT creator_id FROM accounts WHERE platform=? AND handle=? " | |
| 181 | + "AND confidence>=?", (acc.platform, acc.handle, STRONG_THRESHOLD) | |
| 182 | + ).fetchone() | |
| 183 | + if row: | |
| 184 | + return row["creator_id"] | |
| 185 | + return None | |
| 186 | + | |
| 187 | + | |
| 188 | +def sync_source(con: sqlite3.Connection, source_id: str, | |
| 189 | + creators: list[Creator]) -> dict: | |
| 190 | + """Synchronise le lot d'une source : ajouts, fusions, mises à jour, opt-out.""" | |
| 191 | + ts = now_iso() | |
| 192 | + added = updated = skipped_optout = 0 | |
| 193 | + confs: list[float] = [] | |
| 194 | + for cr in creators: | |
| 195 | + cr = ethics.scrub(cr) | |
| 196 | + # opt-out : jamais ré-agrégé (§15) ; si une fiche existe, la masquer | |
| 197 | + if ethics.is_opted_out(cr): | |
| 198 | + skipped_optout += 1 | |
| 199 | + existing = _find_existing(con, cr) | |
| 200 | + if existing: | |
| 201 | + con.execute("UPDATE creators SET status='opted_out', updated_at=? " | |
| 202 | + "WHERE id=?", (ts, existing)) | |
| 203 | + continue | |
| 204 | + confs.extend(a.confidence or 0 for a in cr.platforms) | |
| 205 | + existing_id = _find_existing(con, cr) | |
| 206 | + if existing_id: | |
| 207 | + row = con.execute("SELECT * FROM creators WHERE id=?", | |
| 208 | + (existing_id,)).fetchone() | |
| 209 | + current = creator_from_row(row) | |
| 210 | + if current.status == "opted_out": | |
| 211 | + continue # masqué : on ne ré-agrège pas | |
| 212 | + if should_merge(current, cr): | |
| 213 | + merged = merge_creators(current, cr) | |
| 214 | + _write_creator(con, existing_id, merged, row["first_seen"], ts) | |
| 215 | + updated += 1 | |
| 216 | + continue | |
| 217 | + cid = _unique_id(con, slugify(cr.display_name)) | |
| 218 | + _write_creator(con, cid, cr, ts, ts) | |
| 219 | + added += 1 | |
| 220 | + con.commit() | |
| 221 | + n_acc = sum(len(c.platforms) for c in creators) | |
| 222 | + stats = {"source": source_id, "creators": len(creators), "accounts": n_acc, | |
| 223 | + "added": added, "updated": updated, "optout_skipped": skipped_optout, | |
| 224 | + "avg_confidence": round(sum(confs) / len(confs), 3) if confs else None} | |
| 225 | + alert = None | |
| 226 | + if not creators: | |
| 227 | + alert = "0 créateur retourné — source possiblement bloquée" | |
| 228 | + stats["alert"] = alert | |
| 229 | + con.execute( | |
| 230 | + "INSERT INTO sync_log (ts, source, creators, accounts, avg_confidence," | |
| 231 | + " added, updated, errors, seconds, alert) VALUES (?,?,?,?,?,?,?,0,0,?)", | |
| 232 | + (ts, source_id, len(creators), n_acc, stats["avg_confidence"], | |
| 233 | + added, updated, alert)) | |
| 234 | + con.commit() | |
| 235 | + return stats | |
| 236 | + | |
| 237 | + | |
| 238 | +def log_failure(con: sqlite3.Connection, source_id: str, message: str) -> None: | |
| 239 | + con.execute( | |
| 240 | + "INSERT INTO sync_log (ts, source, creators, accounts, errors, alert)" | |
| 241 | + " VALUES (?,?,0,0,1,?)", (now_iso(), source_id, message[:500])) | |
| 242 | + con.commit() | |
| 243 | + | |
| 244 | + | |
| 245 | +def apply_optout(con: sqlite3.Connection, *, name: str | None = None, | |
| 246 | + account: str | None = None) -> int: | |
| 247 | + """Masque immédiatement les fiches correspondant à une demande de retrait.""" | |
| 248 | + ids: set[str] = set() | |
| 249 | + if account and ":" in account: | |
| 250 | + platform, handle = account.lower().split(":", 1) | |
| 251 | + for row in con.execute( | |
| 252 | + "SELECT creator_id FROM accounts WHERE platform=? AND handle=?", | |
| 253 | + (platform, handle.lstrip("@"))): | |
| 254 | + ids.add(row["creator_id"]) | |
| 255 | + if name: | |
| 256 | + from .dedup import name_key | |
| 257 | + for row in con.execute("SELECT id, display_name FROM creators"): | |
| 258 | + if name_key(row["display_name"]) == name_key(name): | |
| 259 | + ids.add(row["id"]) | |
| 260 | + ts = now_iso() | |
| 261 | + for cid in ids: | |
| 262 | + con.execute("UPDATE creators SET status='opted_out', updated_at=? WHERE id=?", | |
| 263 | + (ts, cid)) | |
| 264 | + con.commit() | |
| 265 | + return len(ids) | |
| 266 | + | |
| 267 | + | |
| 268 | +# --- lecture (API) ---------------------------------------------------------------- | |
| 269 | + | |
| 270 | +_TIERS = ("nano", "micro", "macro", "mega") | |
| 271 | + | |
| 272 | + | |
| 273 | +def search(con: sqlite3.Connection, *, q: str = "", niche: str = "", | |
| 274 | + region: str = "", langue: str = "", plateforme: str = "", | |
| 275 | + tier: str = "", sort: str = "reach", limit: int = 60, | |
| 276 | + offset: int = 0) -> dict: | |
| 277 | + """Recherche annuaire : filtres niche/région/langue/plateforme/taille + texte. | |
| 278 | + | |
| 279 | + Les fiches `opted_out` sont TOUJOURS exclues ; les fiches de mineurs sont | |
| 280 | + exclues de l'annuaire public (prudence extrême, §15). | |
| 281 | + """ | |
| 282 | + where = ["status='active'", "is_minor=0"] | |
| 283 | + params: list = [] | |
| 284 | + if q: | |
| 285 | + where.append("(lower(display_name) LIKE ? OR lower(bio) LIKE ?)") | |
| 286 | + needle = f"%{q.lower()}%" | |
| 287 | + params += [needle, needle] | |
| 288 | + if niche: | |
| 289 | + where.append("(','||niches||',') LIKE ?") | |
| 290 | + params.append(f"%,{niche},%") | |
| 291 | + if region: | |
| 292 | + where.append("region=?") | |
| 293 | + params.append(region) | |
| 294 | + if langue: | |
| 295 | + where.append("(','||languages||',') LIKE ?") | |
| 296 | + params.append(f"%,{langue},%") | |
| 297 | + if tier in _TIERS: | |
| 298 | + where.append("audience_tier=?") | |
| 299 | + params.append(tier) | |
| 300 | + if plateforme: | |
| 301 | + where.append("id IN (SELECT creator_id FROM accounts WHERE platform=? " | |
| 302 | + "AND needs_review=0)") | |
| 303 | + params.append(plateforme) | |
| 304 | + order = {"reach": "total_reach IS NULL, total_reach DESC", | |
| 305 | + "nom": "display_name COLLATE NOCASE ASC", | |
| 306 | + "recent": "updated_at DESC"}.get(sort, "total_reach IS NULL, total_reach DESC") | |
| 307 | + sql_where = " AND ".join(where) | |
| 308 | + total = con.execute(f"SELECT COUNT(*) c FROM creators WHERE {sql_where}", | |
| 309 | + params).fetchone()["c"] | |
| 310 | + rows = con.execute( | |
| 311 | + f"SELECT * FROM creators WHERE {sql_where} ORDER BY {order} LIMIT ? OFFSET ?", | |
| 312 | + params + [max(1, min(200, limit)), max(0, offset)]).fetchall() | |
| 313 | + items = [] | |
| 314 | + for row in rows: | |
| 315 | + accounts = con.execute( | |
| 316 | + "SELECT * FROM accounts WHERE creator_id=? ORDER BY followers DESC", | |
| 317 | + (row["id"],)).fetchall() | |
| 318 | + items.append(_public_dict(row, accounts)) | |
| 319 | + return {"total": total, "count": len(items), "offset": offset, "items": items} | |
| 320 | + | |
| 321 | + | |
| 322 | +def get_creator(con: sqlite3.Connection, cid: str) -> dict | None: | |
| 323 | + row = con.execute( | |
| 324 | + "SELECT * FROM creators WHERE id=? AND status='active' AND is_minor=0", | |
| 325 | + (cid,)).fetchone() | |
| 326 | + if not row: | |
| 327 | + return None | |
| 328 | + accounts = con.execute( | |
| 329 | + "SELECT * FROM accounts WHERE creator_id=? ORDER BY followers DESC", | |
| 330 | + (row["id"],)).fetchall() | |
| 331 | + return _public_dict(row, accounts) | |
| 332 | + | |
| 333 | + | |
| 334 | +def stats(con: sqlite3.Connection) -> dict: | |
| 335 | + """Statistiques d'ensemble pour l'accueil de l'annuaire.""" | |
| 336 | + base = "FROM creators WHERE status='active' AND is_minor=0" | |
| 337 | + n = con.execute(f"SELECT COUNT(*) c {base}").fetchone()["c"] | |
| 338 | + n_acc = con.execute( | |
| 339 | + "SELECT COUNT(*) c FROM accounts a JOIN creators c2 ON c2.id=a.creator_id" | |
| 340 | + " WHERE a.needs_review=0 AND c2.status='active' AND c2.is_minor=0" | |
| 341 | + ).fetchone()["c"] | |
| 342 | + by_platform = {r["platform"]: r["c"] for r in con.execute( | |
| 343 | + "SELECT a.platform, COUNT(*) c FROM accounts a JOIN creators c2 " | |
| 344 | + "ON c2.id=a.creator_id WHERE a.needs_review=0 AND c2.status='active' " | |
| 345 | + "AND c2.is_minor=0 GROUP BY a.platform ORDER BY c DESC")} | |
| 346 | + by_tier = {r["audience_tier"]: r["c"] for r in con.execute( | |
| 347 | + f"SELECT audience_tier, COUNT(*) c {base} GROUP BY audience_tier")} | |
| 348 | + niches: dict[str, int] = {} | |
| 349 | + for row in con.execute(f"SELECT niches {base}"): | |
| 350 | + for nch in (row["niches"] or "").split(","): | |
| 351 | + if nch: | |
| 352 | + niches[nch] = niches.get(nch, 0) + 1 | |
| 353 | + regions: dict[str, int] = {} | |
| 354 | + for row in con.execute(f"SELECT region {base} AND region IS NOT NULL"): | |
| 355 | + regions[row["region"]] = regions.get(row["region"], 0) + 1 | |
| 356 | + last_sync = con.execute( | |
| 357 | + "SELECT ts FROM sync_log ORDER BY id DESC LIMIT 1").fetchone() | |
| 358 | + return {"creators": n, "accounts": n_acc, "by_platform": by_platform, | |
| 359 | + "by_tier": by_tier, | |
| 360 | + "by_niche": dict(sorted(niches.items(), key=lambda x: -x[1])), | |
| 361 | + "by_region": dict(sorted(regions.items(), key=lambda x: -x[1])), | |
| 362 | + "last_sync": last_sync["ts"] if last_sync else None} | |
added
creaka/dedup.py
+93 −0
@@ -0,0 +1,93 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/dedup.py | |
| 4 | +# Desc: Déduplication des créateurs — fusion prudente multi-sources | |
| 5 | +# (CLAUDE.md §12.2 : jamais de fusion sur le seul nom — homonymes !) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Déduplication : un créateur = UNE fiche canonique. | |
| 8 | + | |
| 9 | +Règle de fusion (§12.2) : deux fiches ne fusionnent QUE si elles partagent au | |
| 10 | +moins un compte (plateforme+handle) à confiance élevée des deux côtés. Le nom | |
| 11 | +seul ne suffit JAMAIS — une erreur de fusion mélange deux personnes réelles. | |
| 12 | +""" | |
| 13 | +from __future__ import annotations | |
| 14 | + | |
| 15 | +from .identity import STRONG_THRESHOLD, merge_accounts | |
| 16 | +from .normalize import strip_accents | |
| 17 | +from .schema import Creator | |
| 18 | + | |
| 19 | + | |
| 20 | +def name_key(name: str) -> str: | |
| 21 | + """Clé de comparaison de nom (accents/casse neutralisés) — INDICATIVE seulement.""" | |
| 22 | + return " ".join(strip_accents(name or "").lower().split()) | |
| 23 | + | |
| 24 | + | |
| 25 | +def shared_strong_account(a: Creator, b: Creator) -> bool: | |
| 26 | + """Vrai si a et b partagent ≥1 compte à confiance ≥ STRONG_THRESHOLD des 2 côtés.""" | |
| 27 | + strong_a = {acc.key for acc in a.platforms | |
| 28 | + if (acc.confidence or 0) >= STRONG_THRESHOLD} | |
| 29 | + for acc in b.platforms: | |
| 30 | + if acc.key in strong_a and (acc.confidence or 0) >= STRONG_THRESHOLD: | |
| 31 | + return True | |
| 32 | + return False | |
| 33 | + | |
| 34 | + | |
| 35 | +def should_merge(a: Creator, b: Creator) -> bool: | |
| 36 | + """Décision de fusion : compte fort partagé requis. Homonymes ≠ fusion.""" | |
| 37 | + return shared_strong_account(a, b) | |
| 38 | + | |
| 39 | + | |
| 40 | +def merge_creators(canon: Creator, other: Creator) -> Creator: | |
| 41 | + """Fusionne `other` dans `canon` : union des comptes, meilleure valeur par champ. | |
| 42 | + | |
| 43 | + On ne perd aucun lien vérifié ; les champs descriptifs prennent la valeur | |
| 44 | + la plus riche (bio la plus longue, champ non nul). | |
| 45 | + """ | |
| 46 | + canon.platforms = merge_accounts(canon.platforms, other.platforms) | |
| 47 | + canon.source_ids = sorted(set(canon.source_ids) | set(other.source_ids)) | |
| 48 | + if len(other.bio or "") > len(canon.bio or ""): | |
| 49 | + canon.bio = other.bio | |
| 50 | + if len(other.notes or "") > len(canon.notes or ""): | |
| 51 | + canon.notes = other.notes | |
| 52 | + canon.niches = sorted(set(canon.niches) | set(other.niches)) or ["autre"] | |
| 53 | + canon.languages = sorted(set(canon.languages) | set(other.languages)) or ["fr"] | |
| 54 | + for field_ in ("region", "city", "legal_name", "business_contact", | |
| 55 | + "agency", "link_in_bio_url", "avatar_url"): | |
| 56 | + if getattr(canon, field_) in (None, "") and getattr(other, field_): | |
| 57 | + setattr(canon, field_, getattr(other, field_)) | |
| 58 | + # prudence maximale : si UNE source signale un mineur, le régime s'applique | |
| 59 | + canon.is_minor = canon.is_minor or other.is_minor | |
| 60 | + # opt-out prime sur tout (§15) | |
| 61 | + if "opted_out" in (canon.status, other.status): | |
| 62 | + canon.status = "opted_out" | |
| 63 | + # recalculer les agrégats (tier, portée, plateforme principale) | |
| 64 | + canon.audience_tier = "" | |
| 65 | + canon.primary_platform = "" | |
| 66 | + canon.total_reach = None | |
| 67 | + return canon.finalize() | |
| 68 | + | |
| 69 | + | |
| 70 | +def dedupe(creators: list[Creator]) -> list[Creator]: | |
| 71 | + """Déduplique une liste de fiches (intra-lot). Utilisé par l'ingestion. | |
| 72 | + | |
| 73 | + Index par clé de compte : deux fiches partageant un compte fort fusionnent. | |
| 74 | + """ | |
| 75 | + canon_by_acc: dict[str, Creator] = {} | |
| 76 | + result: list[Creator] = [] | |
| 77 | + for cr in creators: | |
| 78 | + target = None | |
| 79 | + for acc in cr.platforms: | |
| 80 | + if (acc.confidence or 0) >= STRONG_THRESHOLD and acc.key in canon_by_acc: | |
| 81 | + cand = canon_by_acc[acc.key] | |
| 82 | + if should_merge(cand, cr): | |
| 83 | + target = cand | |
| 84 | + break | |
| 85 | + if target is None: | |
| 86 | + result.append(cr) | |
| 87 | + target = cr | |
| 88 | + else: | |
| 89 | + merge_creators(target, cr) | |
| 90 | + for acc in target.platforms: | |
| 91 | + if (acc.confidence or 0) >= STRONG_THRESHOLD: | |
| 92 | + canon_by_acc[acc.key] = target | |
| 93 | + return result | |
added
creaka/ethics.py
+114 −0
@@ -0,0 +1,114 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/ethics.py | |
| 4 | +# Desc: Vie privée & éthique — garde anti-données-privées, opt-out persistant, | |
| 5 | +# régime restreint des mineurs (CLAUDE.md §15 — PRIME SUR TOUT) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Contrôles éthiques appliqués à CHAQUE fiche avant stockage. | |
| 8 | + | |
| 9 | +- `scrub()` : purge toute donnée d'apparence privée (téléphones, adresses | |
| 10 | + civiques, courriels non déclarés comme professionnels publics). | |
| 11 | +- Opt-out : liste de suppression persistante (data/optout.json). Une fiche | |
| 12 | + opted_out est masquée et EXCLUE des ré-agrégations futures. | |
| 13 | +- Mineurs : régime restreint (pas de contact, pas de localisation précise). | |
| 14 | +""" | |
| 15 | +from __future__ import annotations | |
| 16 | + | |
| 17 | +import json | |
| 18 | +import re | |
| 19 | +import threading | |
| 20 | +from pathlib import Path | |
| 21 | + | |
| 22 | +from .dedup import name_key | |
| 23 | +from .schema import Creator | |
| 24 | + | |
| 25 | +ROOT = Path(__file__).resolve().parent.parent | |
| 26 | +OPTOUT_PATH = ROOT / "data" / "optout.json" | |
| 27 | +OPTOUT_REQUESTS_PATH = ROOT / "data" / "optout_requests.jsonl" | |
| 28 | + | |
| 29 | +_lock = threading.Lock() | |
| 30 | + | |
| 31 | +# téléphone nord-américain / adresse civique — n'ont RIEN à faire dans une bio | |
| 32 | +_PHONE_RE = re.compile(r"\(?\b\d{3}\)?[\s.\-]\d{3}[\s.\-]\d{4}\b") | |
| 33 | +_CIVIC_RE = re.compile( | |
| 34 | + r"\b\d{1,5},?\s+(?:rue|avenue|av\.|boulevard|boul\.|chemin|app\.?|apt\.?)\s", re.I) | |
| 35 | +_EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+") | |
| 36 | +# domaines de courriel personnel : jamais conservés même si trouvés en clair | |
| 37 | +_PERSONAL_EMAIL_DOMAINS = ("gmail.", "hotmail.", "outlook.", "yahoo.", "icloud.", | |
| 38 | + "videotron.", "live.", "protonmail.", "me.com") | |
| 39 | + | |
| 40 | + | |
| 41 | +def scrub(creator: Creator) -> Creator: | |
| 42 | + """Purge les données privées d'une fiche (bio, notes, contact). Idempotent.""" | |
| 43 | + for field_ in ("bio", "notes"): | |
| 44 | + text = getattr(creator, field_) or "" | |
| 45 | + text = _PHONE_RE.sub("[retiré]", text) | |
| 46 | + text = _CIVIC_RE.sub("[retiré] ", text) | |
| 47 | + text = _EMAIL_RE.sub("[retiré]", text) | |
| 48 | + setattr(creator, field_, text) | |
| 49 | + # business_contact : uniquement un courriel PRO affiché publiquement. | |
| 50 | + # Un domaine grand public (gmail, hotmail…) est traité comme personnel → retiré. | |
| 51 | + bc = creator.business_contact | |
| 52 | + if bc: | |
| 53 | + bc = bc.strip().lower() | |
| 54 | + if not _EMAIL_RE.fullmatch(bc) or any(d in bc for d in _PERSONAL_EMAIL_DOMAINS): | |
| 55 | + creator.business_contact = None | |
| 56 | + # régime restreint des mineurs (§15) | |
| 57 | + if creator.is_minor: | |
| 58 | + creator.business_contact = None | |
| 59 | + creator.city = None | |
| 60 | + creator.region = None | |
| 61 | + creator.legal_name = None | |
| 62 | + return creator | |
| 63 | + | |
| 64 | + | |
| 65 | +# --- opt-out ------------------------------------------------------------------- | |
| 66 | + | |
| 67 | +def _load_optout() -> dict: | |
| 68 | + if OPTOUT_PATH.exists(): | |
| 69 | + try: | |
| 70 | + return json.loads(OPTOUT_PATH.read_text(encoding="utf-8")) | |
| 71 | + except Exception: | |
| 72 | + pass | |
| 73 | + return {"names": [], "accounts": []} | |
| 74 | + | |
| 75 | + | |
| 76 | +def optout_keys() -> tuple[set[str], set[str]]: | |
| 77 | + """(clés de noms, clés de comptes plateforme:handle) de la liste de suppression.""" | |
| 78 | + data = _load_optout() | |
| 79 | + names = {name_key(n) for n in data.get("names", []) if n} | |
| 80 | + accounts = {a.strip().lower() for a in data.get("accounts", []) if a} | |
| 81 | + return names, accounts | |
| 82 | + | |
| 83 | + | |
| 84 | +def is_opted_out(creator: Creator) -> bool: | |
| 85 | + """Vrai si la fiche correspond à la liste de suppression persistante. | |
| 86 | + | |
| 87 | + Correspondance par COMPTE (plateforme:handle — fiable) ou par nom exact | |
| 88 | + (filet de sécurité : on préfère masquer trop que pas assez, §15). | |
| 89 | + """ | |
| 90 | + names, accounts = optout_keys() | |
| 91 | + if name_key(creator.display_name) in names: | |
| 92 | + return True | |
| 93 | + return any(acc.key in accounts for acc in creator.platforms) | |
| 94 | + | |
| 95 | + | |
| 96 | +def add_optout(name: str | None = None, account: str | None = None, | |
| 97 | + reason: str = "", contact: str = "") -> None: | |
| 98 | + """Ajoute une entrée à la liste de suppression + journalise la demande.""" | |
| 99 | + with _lock: | |
| 100 | + data = _load_optout() | |
| 101 | + if name and name_key(name) not in {name_key(n) for n in data["names"]}: | |
| 102 | + data["names"].append(name) | |
| 103 | + if account: | |
| 104 | + account = account.strip().lower() | |
| 105 | + if account not in data["accounts"]: | |
| 106 | + data["accounts"].append(account) | |
| 107 | + OPTOUT_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| 108 | + OPTOUT_PATH.write_text( | |
| 109 | + json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8") | |
| 110 | + from .schema import now_iso | |
| 111 | + with OPTOUT_REQUESTS_PATH.open("a", encoding="utf-8") as fh: | |
| 112 | + fh.write(json.dumps({"ts": now_iso(), "name": name, "account": account, | |
| 113 | + "reason": reason[:500], "contact": contact[:200]}, | |
| 114 | + ensure_ascii=False) + "\n") | |
added
creaka/identity.py
+81 −0
@@ -0,0 +1,81 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/identity.py | |
| 4 | +# Desc: Résolution d'identité — rattacher les bons comptes à la bonne personne | |
| 5 | +# avec un score de confiance par signal (CLAUDE.md §12.1) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Résolution d'identité : LE cœur du projet. | |
| 8 | + | |
| 9 | +Chaque rattachement de compte porte un score `confidence` selon le signal qui | |
| 10 | +l'a produit (du plus fort au plus faible, §12.1). Sous `REVIEW_THRESHOLD`, le | |
| 11 | +compte est marqué « à vérifier » et EXCLU de la fiche publique plutôt que | |
| 12 | +rattaché à tort — mieux vaut un lien vérifié de moins qu'un lien erroné. | |
| 13 | +""" | |
| 14 | +from __future__ import annotations | |
| 15 | + | |
| 16 | +from .schema import PlatformAccount | |
| 17 | + | |
| 18 | +# signaux §12.1, du plus fort au plus faible | |
| 19 | +SIGNAL_CONFIDENCE: dict[str, float] = { | |
| 20 | + "profil_source": 0.98, # le compte DONT la fiche est issue (attribution inhérente) | |
| 21 | + "link_in_bio": 0.95, # page Linktree/Beacons du créateur listant le compte | |
| 22 | + "api_officielle": 0.95, # confirmé par l'API officielle de la plateforme | |
| 23 | + "cross_link": 0.90, # un profil vérifié pointe vers l'autre (bio IG → TikTok) | |
| 24 | + "listes_medias": 0.85, # listes/palmarès médias croisés (plusieurs sources concordantes) | |
| 25 | + "base_publique": 0.85, # base de connaissances publique curée (Wikidata/Wikipédia) | |
| 26 | + "handle_match": 0.75, # même handle + même nom d'affichage sur 2 plateformes | |
| 27 | + "mention": 0.60, # mentions réciproques / mêmes liens externes | |
| 28 | + "inconnu": 0.40, | |
| 29 | +} | |
| 30 | + | |
| 31 | +# sous ce seuil : « à vérifier », jamais publié comme rattaché | |
| 32 | +REVIEW_THRESHOLD = 0.50 | |
| 33 | +# à partir de ce seuil : compte considéré fiable pour la déduplication (§12.2) | |
| 34 | +STRONG_THRESHOLD = 0.75 | |
| 35 | + | |
| 36 | + | |
| 37 | +def confidence_for(signal: str) -> float: | |
| 38 | + return SIGNAL_CONFIDENCE.get(signal, SIGNAL_CONFIDENCE["inconnu"]) | |
| 39 | + | |
| 40 | + | |
| 41 | +def account(platform: str, handle: str, signal: str, *, | |
| 42 | + followers: int | None = None, url: str = "", | |
| 43 | + verified: bool | None = None) -> PlatformAccount: | |
| 44 | + """Fabrique un compte avec la confiance du signal fourni (helper connecteurs).""" | |
| 45 | + return PlatformAccount(platform=platform, handle=handle, url=url, | |
| 46 | + followers=followers, verified=verified, | |
| 47 | + confidence=confidence_for(signal), signal=signal) | |
| 48 | + | |
| 49 | + | |
| 50 | +def needs_review(acc: PlatformAccount) -> bool: | |
| 51 | + return (acc.confidence or 0.0) < REVIEW_THRESHOLD | |
| 52 | + | |
| 53 | + | |
| 54 | +def is_strong(acc: PlatformAccount) -> bool: | |
| 55 | + return (acc.confidence or 0.0) >= STRONG_THRESHOLD | |
| 56 | + | |
| 57 | + | |
| 58 | +def merge_accounts(existing: list[PlatformAccount], | |
| 59 | + incoming: list[PlatformAccount]) -> list[PlatformAccount]: | |
| 60 | + """Union de deux listes de comptes — on ne perd AUCUN lien vérifié (§12.2). | |
| 61 | + | |
| 62 | + Même compte (plateforme+handle) : la meilleure confiance gagne ; les champs | |
| 63 | + factuels (abonnés, badge) prennent la valeur la plus fraîche non nulle. | |
| 64 | + """ | |
| 65 | + by_key: dict[str, PlatformAccount] = {a.key: a for a in existing} | |
| 66 | + for acc in incoming: | |
| 67 | + prev = by_key.get(acc.key) | |
| 68 | + if prev is None: | |
| 69 | + by_key[acc.key] = acc | |
| 70 | + continue | |
| 71 | + best = acc if (acc.confidence or 0) >= (prev.confidence or 0) else prev | |
| 72 | + other = prev if best is acc else acc | |
| 73 | + if best.followers is None: | |
| 74 | + best.followers = other.followers | |
| 75 | + if best.verified is None: | |
| 76 | + best.verified = other.verified | |
| 77 | + best.metrics = {**other.metrics, **best.metrics} | |
| 78 | + best.last_checked = max(best.last_checked, other.last_checked) | |
| 79 | + by_key[acc.key] = best | |
| 80 | + return sorted(by_key.values(), | |
| 81 | + key=lambda a: (a.followers is None, -(a.followers or 0))) | |
added
creaka/ingest.py
+100 −0
@@ -0,0 +1,100 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/ingest.py | |
| 4 | +# Desc: Pipeline d'ingestion — découverte puis enrichissement par plateforme, | |
| 5 | +# synchronisation de la base (ajouts / fusions / mises à jour) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Pipeline Créa-Ka (calqué sur louka/ingest.py). | |
| 8 | + | |
| 9 | +Ordre d'un passage complet : | |
| 10 | +1. connecteurs DÉCOUVERTE (listes médias, hashtags, agences…) → nouvelles fiches ; | |
| 11 | +2. connecteurs ENRICHISSEMENT par plateforme (instagram, tiktok, youtube, | |
| 12 | + twitch) → métriques fraîches + liens croisés, créateur par créateur ; | |
| 13 | +3. link-in-bio EN DERNIER : il consomme les `link_in_bio_url` découvertes à | |
| 14 | + l'étape 2 et révèle d'un coup tous les comptes restants (§8-9). | |
| 15 | + | |
| 16 | +Logging structuré par source : créateurs, comptes, confiance moyenne, durée, | |
| 17 | +erreurs (§16) ; alerte si une source ne retourne plus rien (§18 monitoring). | |
| 18 | +""" | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import sys | |
| 22 | +import time | |
| 23 | +import traceback | |
| 24 | + | |
| 25 | +from . import db | |
| 26 | +from .connectors import CONNECTORS | |
| 27 | +from .dedup import dedupe | |
| 28 | + | |
| 29 | +# ordre d'exécution des connecteurs d'enrichissement (link-in-bio en dernier) | |
| 30 | +ENRICH_ORDER = ["instagram-profil", "tiktok-profil", "youtube", "twitch", | |
| 31 | + "link-in-bio"] | |
| 32 | + | |
| 33 | + | |
| 34 | +def _finalize_batch(creators: list) -> tuple[list, int]: | |
| 35 | + finalized, dropped = [], 0 | |
| 36 | + for cr in creators: | |
| 37 | + try: | |
| 38 | + finalized.append(cr.finalize()) | |
| 39 | + except Exception: # une fiche malformée ne bloque pas la source | |
| 40 | + dropped += 1 | |
| 41 | + return finalized, dropped | |
| 42 | + | |
| 43 | + | |
| 44 | +def load_active(con) -> list: | |
| 45 | + """Fiches actives (jamais les opted_out : exclues des ré-agrégations, §15).""" | |
| 46 | + rows = con.execute( | |
| 47 | + "SELECT * FROM creators WHERE status='active' ORDER BY total_reach DESC" | |
| 48 | + ).fetchall() | |
| 49 | + return [db.creator_from_row(r) for r in rows] | |
| 50 | + | |
| 51 | + | |
| 52 | +def run(sources: list[str] | None = None) -> list[dict]: | |
| 53 | + """Exécute l'ingestion pour toutes les sources (ou celles demandées).""" | |
| 54 | + con = db.connect() | |
| 55 | + results: list[dict] = [] | |
| 56 | + discovery = [sid for sid, cls in CONNECTORS.items() if cls.kind == "discovery"] | |
| 57 | + enrichment = [sid for sid in ENRICH_ORDER if sid in CONNECTORS] | |
| 58 | + enrichment += [sid for sid, cls in CONNECTORS.items() | |
| 59 | + if cls.kind == "enrichment" and sid not in enrichment] | |
| 60 | + targets = sources or (discovery + enrichment) | |
| 61 | + | |
| 62 | + for sid in targets: | |
| 63 | + cls = CONNECTORS.get(sid) | |
| 64 | + if cls is None: | |
| 65 | + print(f"[crea-ka] connecteur inconnu : {sid}", file=sys.stderr) | |
| 66 | + continue | |
| 67 | + t0 = time.time() | |
| 68 | + print(f"[crea-ka] sync {sid} ({cls.kind}) ...") | |
| 69 | + try: | |
| 70 | + connector = cls() | |
| 71 | + if cls.kind == "discovery": | |
| 72 | + batch = connector.fetch() | |
| 73 | + else: | |
| 74 | + batch = connector.enrich(load_active(con)) | |
| 75 | + finalized, dropped = _finalize_batch(batch) | |
| 76 | + finalized = dedupe(finalized) # dédup intra-lot avant la base | |
| 77 | + stats = db.sync_source(con, sid, finalized) | |
| 78 | + stats["seconds"] = round(time.time() - t0, 1) | |
| 79 | + if dropped: | |
| 80 | + stats["dropped"] = dropped | |
| 81 | + if stats.get("alert") and cls.kind == "discovery": | |
| 82 | + print(f"[crea-ka] ⚠ ALERTE {sid} : {stats['alert']}") | |
| 83 | + print(f"[crea-ka] {stats}") | |
| 84 | + results.append(stats) | |
| 85 | + except Exception as exc: # robustesse : une source ne bloque pas les autres | |
| 86 | + db.log_failure(con, sid, f"{exc}") | |
| 87 | + traceback.print_exc() | |
| 88 | + results.append({"source": sid, "error": str(exc)}) | |
| 89 | + return results | |
| 90 | + | |
| 91 | + | |
| 92 | +def watch(interval_seconds: int = 86400) -> None: | |
| 93 | + """Synchronisation en boucle (cadence §14 — défaut : quotidienne).""" | |
| 94 | + while True: | |
| 95 | + try: | |
| 96 | + run() | |
| 97 | + except Exception: | |
| 98 | + traceback.print_exc() | |
| 99 | + print(f"[crea-ka] prochain passage dans {interval_seconds}s") | |
| 100 | + time.sleep(interval_seconds) | |
added
creaka/normalize.py
+362 −0
@@ -0,0 +1,362 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/normalize.py | |
| 4 | +# Desc: Normalisation commune — handles, URLs canoniques par plateforme, | |
| 5 | +# niches, régions, langue, taille d'audience (voir CLAUDE.md §6, §7, §11) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Couche de normalisation partagée par tous les connecteurs. | |
| 8 | + | |
| 9 | +Chaque connecteur remplit des champs bruts ; `schema.Creator.finalize()` | |
| 10 | +appelle ces fonctions pour produire une fiche canonique. Idempotent. | |
| 11 | +""" | |
| 12 | +from __future__ import annotations | |
| 13 | + | |
| 14 | +import re | |
| 15 | +import unicodedata | |
| 16 | + | |
| 17 | +# --- taxonomies (CLAUDE.md §6) ------------------------------------------------ | |
| 18 | +NICHES = { | |
| 19 | + "humour", "mode", "beaute", "lifestyle", "famille-parentalite", "cuisine", | |
| 20 | + "gaming", "tech", "sport-fitness", "plein-air", "voyage", "musique", "arts", | |
| 21 | + "danse", "education", "finance-affaires", "sante-mieux-etre", "bouffe-resto", | |
| 22 | + "actualite-opinion", "autre", | |
| 23 | +} | |
| 24 | + | |
| 25 | +PLATFORMS = { | |
| 26 | + "instagram", "tiktok", "youtube", "twitch", "kick", "x", "facebook", | |
| 27 | + "snapchat", "substack", "patreon", "onlyfans", "linkedin", "threads", | |
| 28 | + "podcast", "site-web", "autre", | |
| 29 | +} | |
| 30 | + | |
| 31 | +CREATOR_TYPES = { | |
| 32 | + "influenceur", "youtubeur", "streamer", "podcasteur", "createur-tiktok", | |
| 33 | + "createur-ecrit", "musicien", "humoriste", "autre", | |
| 34 | +} | |
| 35 | + | |
| 36 | +LANGUAGES = {"fr", "en", "bilingue"} | |
| 37 | + | |
| 38 | +REGIONS = { | |
| 39 | + "Bas-Saint-Laurent", "Saguenay–Lac-Saint-Jean", "Capitale-Nationale", | |
| 40 | + "Mauricie", "Estrie", "Montréal", "Outaouais", "Abitibi-Témiscamingue", | |
| 41 | + "Côte-Nord", "Nord-du-Québec", "Gaspésie–Îles-de-la-Madeleine", | |
| 42 | + "Chaudière-Appalaches", "Laval", "Lanaudière", "Laurentides", | |
| 43 | + "Montérégie", "Centre-du-Québec", | |
| 44 | +} | |
| 45 | + | |
| 46 | +# --- URLs canoniques par plateforme (CLAUDE.md §11) --------------------------- | |
| 47 | +# gabarit : handle SANS @ ; None = pas de forme canonique (garder l'URL source) | |
| 48 | +_URL_TEMPLATES: dict[str, str | None] = { | |
| 49 | + "instagram": "https://www.instagram.com/{h}/", | |
| 50 | + "tiktok": "https://www.tiktok.com/@{h}", | |
| 51 | + "youtube": "https://www.youtube.com/@{h}", | |
| 52 | + "twitch": "https://www.twitch.tv/{h}", | |
| 53 | + "kick": "https://kick.com/{h}", | |
| 54 | + "x": "https://x.com/{h}", | |
| 55 | + "facebook": "https://www.facebook.com/{h}", | |
| 56 | + "snapchat": "https://www.snapchat.com/add/{h}", | |
| 57 | + "substack": "https://{h}.substack.com", | |
| 58 | + "patreon": "https://www.patreon.com/{h}", | |
| 59 | + "onlyfans": "https://onlyfans.com/{h}", | |
| 60 | + "linkedin": "https://www.linkedin.com/in/{h}", | |
| 61 | + "threads": "https://www.threads.net/@{h}", | |
| 62 | + "podcast": None, | |
| 63 | + "site-web": None, | |
| 64 | + "autre": None, | |
| 65 | +} | |
| 66 | + | |
| 67 | +# détection de plateforme à partir d'une URL (pages link-in-bio, bios, etc.) | |
| 68 | +_URL_PLATFORM_RES: list[tuple[str, re.Pattern]] = [ | |
| 69 | + ("instagram", re.compile(r"instagram\.com/([\w.\-]+)", re.I)), | |
| 70 | + ("tiktok", re.compile(r"tiktok\.com/@([\w.\-]+)", re.I)), | |
| 71 | + ("youtube", re.compile(r"youtube\.com/(?:@|c/|user/|channel/)([\w.\-]+)", re.I)), | |
| 72 | + ("youtube", re.compile(r"youtu\.be/([\w.\-]+)", re.I)), | |
| 73 | + ("twitch", re.compile(r"twitch\.tv/([\w.\-]+)", re.I)), | |
| 74 | + ("kick", re.compile(r"kick\.com/([\w.\-]+)", re.I)), | |
| 75 | + ("x", re.compile(r"(?:twitter|x)\.com/([\w.\-]+)", re.I)), | |
| 76 | + ("facebook", re.compile(r"facebook\.com/([\w.\-]+)", re.I)), | |
| 77 | + ("snapchat", re.compile(r"snapchat\.com/add/([\w.\-]+)", re.I)), | |
| 78 | + ("substack", re.compile(r"https?://([\w\-]+)\.substack\.com", re.I)), | |
| 79 | + ("patreon", re.compile(r"patreon\.com/([\w.\-]+)", re.I)), | |
| 80 | + ("onlyfans", re.compile(r"onlyfans\.com/([\w.\-]+)", re.I)), | |
| 81 | + ("linkedin", re.compile(r"linkedin\.com/in/([\w.\-]+)", re.I)), | |
| 82 | + ("threads", re.compile(r"threads\.net/@?([\w.\-]+)", re.I)), | |
| 83 | +] | |
| 84 | + | |
| 85 | +# chemins qui ne sont PAS des handles (pages génériques des plateformes) | |
| 86 | +_NOT_HANDLES = { | |
| 87 | + "p", "reel", "reels", "stories", "share", "watch", "video", "videos", | |
| 88 | + "playlist", "shorts", "live", "intent", "hashtag", "explore", "home", | |
| 89 | + "profile.php", "groups", "pages", "events", "posts", "status", "search", | |
| 90 | +} | |
| 91 | + | |
| 92 | + | |
| 93 | +def strip_accents(text: str) -> str: | |
| 94 | + """« Bédard » → « Bedard » (clés de comparaison, jamais pour l'affichage).""" | |
| 95 | + return "".join(c for c in unicodedata.normalize("NFD", text) | |
| 96 | + if unicodedata.category(c) != "Mn") | |
| 97 | + | |
| 98 | + | |
| 99 | +def slugify(text: str) -> str: | |
| 100 | + """Nom d'affichage → slug d'identifiant interne (ex. « Enola Bédard » → enola-bedard).""" | |
| 101 | + s = strip_accents(text or "").lower() | |
| 102 | + s = re.sub(r"[^a-z0-9]+", "-", s).strip("-") | |
| 103 | + return s or "inconnu" | |
| 104 | + | |
| 105 | + | |
| 106 | +def clean_handle(raw: str | None) -> str: | |
| 107 | + """Nettoie un handle brut : retire @, URL, espaces, barre finale ; minuscules. | |
| 108 | + | |
| 109 | + Les handles des grandes plateformes sont insensibles à la casse — on | |
| 110 | + normalise en minuscules pour la comparaison ET la forme canonique. | |
| 111 | + """ | |
| 112 | + if not raw: | |
| 113 | + return "" | |
| 114 | + h = raw.strip() | |
| 115 | + # si on a reçu une URL complète, garder le dernier segment utile | |
| 116 | + if "://" in h or h.startswith("www."): | |
| 117 | + h = h.rstrip("/").split("/")[-1] | |
| 118 | + h = h.lstrip("@").split("?")[0].strip().strip("/") | |
| 119 | + # ID de chaîne YouTube (UC…) : sensible à la casse — ne JAMAIS minusculiser | |
| 120 | + if re.fullmatch(r"UC[0-9A-Za-z_-]{22}", h): | |
| 121 | + return h | |
| 122 | + return h.lower() | |
| 123 | + | |
| 124 | + | |
| 125 | +def normalize_platform(raw: str | None) -> str: | |
| 126 | + """Nom de plateforme brut → valeur canonique de §6.2 (défaut : « autre »).""" | |
| 127 | + if not raw: | |
| 128 | + return "autre" | |
| 129 | + p = strip_accents(raw).lower().strip() | |
| 130 | + aliases = { | |
| 131 | + "ig": "instagram", "insta": "instagram", "instagram": "instagram", | |
| 132 | + "tiktok": "tiktok", "tik-tok": "tiktok", "tik tok": "tiktok", | |
| 133 | + "youtube": "youtube", "yt": "youtube", | |
| 134 | + "twitch": "twitch", "kick": "kick", | |
| 135 | + "twitter": "x", "x": "x", | |
| 136 | + "facebook": "facebook", "fb": "facebook", | |
| 137 | + "snapchat": "snapchat", "snap": "snapchat", | |
| 138 | + "substack": "substack", "patreon": "patreon", "onlyfans": "onlyfans", | |
| 139 | + "of": "onlyfans", | |
| 140 | + "linkedin": "linkedin", "threads": "threads", | |
| 141 | + "podcast": "podcast", "balado": "podcast", | |
| 142 | + "site": "site-web", "site-web": "site-web", "website": "site-web", | |
| 143 | + "web": "site-web", "blogue": "site-web", "blog": "site-web", | |
| 144 | + } | |
| 145 | + return aliases.get(p, p if p in PLATFORMS else "autre") | |
| 146 | + | |
| 147 | + | |
| 148 | +def canonical_url(platform: str, handle: str, fallback: str = "") -> str: | |
| 149 | + """Forme canonique de l'URL d'un compte (CLAUDE.md §11).""" | |
| 150 | + h = clean_handle(handle) | |
| 151 | + # chaîne YouTube identifiée par son ID (pas de @handle connu) | |
| 152 | + if platform == "youtube" and re.fullmatch(r"UC[0-9A-Za-z_-]{22}", h): | |
| 153 | + return f"https://www.youtube.com/channel/{h}" | |
| 154 | + tpl = _URL_TEMPLATES.get(platform) | |
| 155 | + if tpl and h: | |
| 156 | + return tpl.format(h=h) | |
| 157 | + return fallback or "" | |
| 158 | + | |
| 159 | + | |
| 160 | +def platform_from_url(url: str) -> tuple[str, str] | None: | |
| 161 | + """URL → (plateforme, handle) si reconnue, sinon None. | |
| 162 | + | |
| 163 | + Utilisé par le connecteur link-in-bio : chaque lien d'une page Linktree | |
| 164 | + est classé ici. Les chemins génériques (posts, vidéos) sont rejetés. | |
| 165 | + """ | |
| 166 | + if not url: | |
| 167 | + return None | |
| 168 | + for platform, rx in _URL_PLATFORM_RES: | |
| 169 | + m = rx.search(url) | |
| 170 | + if m: | |
| 171 | + handle = clean_handle(m.group(1)) | |
| 172 | + if not handle or handle in _NOT_HANDLES: | |
| 173 | + return None | |
| 174 | + return platform, handle | |
| 175 | + return None | |
| 176 | + | |
| 177 | + | |
| 178 | +def clean_name(raw: str | None) -> str: | |
| 179 | + """Nettoie un nom d'affichage (espaces, emojis décoratifs en bordure).""" | |
| 180 | + if not raw: | |
| 181 | + return "" | |
| 182 | + name = re.sub(r"\s+", " ", raw).strip() | |
| 183 | + # retirer les emojis/symboles en tête et en queue seulement (jamais au milieu : | |
| 184 | + # certains noms de scène en contiennent volontairement) | |
| 185 | + name = re.sub(r"^[\W_]+(?=\w)", "", name) | |
| 186 | + name = re.sub(r"(?<=\w)[^\w).!?'’\"]+$", "", name) | |
| 187 | + return name.strip() | |
| 188 | + | |
| 189 | + | |
| 190 | +def clean_bio(raw: str | None) -> str: | |
| 191 | + """Bio publique : espaces normalisés, longueur bornée.""" | |
| 192 | + if not raw: | |
| 193 | + return "" | |
| 194 | + bio = re.sub(r"[ \t]+", " ", raw).strip() | |
| 195 | + return bio[:1200] | |
| 196 | + | |
| 197 | + | |
| 198 | +def map_niche(raw: str | None) -> str: | |
| 199 | + """Catégorie source → niche canonique (§6.1).""" | |
| 200 | + if not raw: | |
| 201 | + return "autre" | |
| 202 | + c = strip_accents(raw).lower().strip() | |
| 203 | + mapping = { | |
| 204 | + "humour": "humour", "comedie": "humour", "comedy": "humour", | |
| 205 | + "mode": "mode", "fashion": "mode", | |
| 206 | + "beaute": "beaute", "beauty": "beaute", "makeup": "beaute", | |
| 207 | + "lifestyle": "lifestyle", "tele-realite": "lifestyle", | |
| 208 | + "famille": "famille-parentalite", "parentalite": "famille-parentalite", | |
| 209 | + "famille-parentalite": "famille-parentalite", "maman": "famille-parentalite", | |
| 210 | + "cuisine": "cuisine", "food": "bouffe-resto", "bouffe": "bouffe-resto", | |
| 211 | + "bouffe-resto": "bouffe-resto", "resto": "bouffe-resto", | |
| 212 | + "gaming": "gaming", "jeux video": "gaming", "jeux-video": "gaming", | |
| 213 | + "tech": "tech", "technologie": "tech", | |
| 214 | + "sport": "sport-fitness", "fitness": "sport-fitness", | |
| 215 | + "sport-fitness": "sport-fitness", "entrainement": "sport-fitness", | |
| 216 | + "plein air": "plein-air", "plein-air": "plein-air", "chasse": "plein-air", | |
| 217 | + "peche": "plein-air", "outdoor": "plein-air", | |
| 218 | + "voyage": "voyage", "travel": "voyage", | |
| 219 | + "musique": "musique", "music": "musique", | |
| 220 | + "arts": "arts", "art": "arts", | |
| 221 | + "danse": "danse", "dance": "danse", | |
| 222 | + "education": "education", | |
| 223 | + "finance": "finance-affaires", "affaires": "finance-affaires", | |
| 224 | + "finance-affaires": "finance-affaires", "entrepreneur": "finance-affaires", | |
| 225 | + "business": "finance-affaires", "immobilier": "finance-affaires", | |
| 226 | + "sante": "sante-mieux-etre", "mieux-etre": "sante-mieux-etre", | |
| 227 | + "sante-mieux-etre": "sante-mieux-etre", "bien-etre": "sante-mieux-etre", | |
| 228 | + "actualite": "actualite-opinion", "opinion": "actualite-opinion", | |
| 229 | + "actualite-opinion": "actualite-opinion", "medias": "actualite-opinion", | |
| 230 | + "podcast": "actualite-opinion", | |
| 231 | + } | |
| 232 | + return mapping.get(c, c if c in NICHES else "autre") | |
| 233 | + | |
| 234 | + | |
| 235 | +# localisation publique déclarée → région administrative (§7). | |
| 236 | +# ⚠️ On ne mappe QUE ce qui est sans ambiguïté ; « Québec » seul désigne la | |
| 237 | +# PROVINCE dans les listes médias → region None. Jamais d'inférence privée. | |
| 238 | +_CITY_REGION = { | |
| 239 | + "montreal": "Montréal", "lachine": "Montréal", "verdun": "Montréal", | |
| 240 | + "quebec (ville)": "Capitale-Nationale", "ville de quebec": "Capitale-Nationale", | |
| 241 | + "charlevoix": "Capitale-Nationale", | |
| 242 | + "laval": "Laval", | |
| 243 | + "levis": "Chaudière-Appalaches", "beauce": "Chaudière-Appalaches", | |
| 244 | + "gatineau": "Outaouais", | |
| 245 | + "sherbrooke": "Estrie", "waterloo": "Estrie", # Haute-Yamaska → Estrie (2021) | |
| 246 | + "granby": "Estrie", | |
| 247 | + "trois-rivieres": "Mauricie", "shawinigan": "Mauricie", | |
| 248 | + "saguenay": "Saguenay–Lac-Saint-Jean", "chicoutimi": "Saguenay–Lac-Saint-Jean", | |
| 249 | + "alma": "Saguenay–Lac-Saint-Jean", | |
| 250 | + "rimouski": "Bas-Saint-Laurent", "riviere-du-loup": "Bas-Saint-Laurent", | |
| 251 | + "gaspe": "Gaspésie–Îles-de-la-Madeleine", | |
| 252 | + "rouyn-noranda": "Abitibi-Témiscamingue", "val-d'or": "Abitibi-Témiscamingue", | |
| 253 | + "drummondville": "Centre-du-Québec", "victoriaville": "Centre-du-Québec", | |
| 254 | + "longueuil": "Montérégie", "brossard": "Montérégie", | |
| 255 | + "saint-jean-sur-richelieu": "Montérégie", | |
| 256 | + "blainville": "Laurentides", "pointe-calumet": "Laurentides", | |
| 257 | + "saint-jerome": "Laurentides", "mont-tremblant": "Laurentides", | |
| 258 | + "terrebonne": "Lanaudière", "repentigny": "Lanaudière", "joliette": "Lanaudière", | |
| 259 | + "sept-iles": "Côte-Nord", "baie-comeau": "Côte-Nord", | |
| 260 | +} | |
| 261 | + | |
| 262 | + | |
| 263 | +def map_region(location: str | None) -> tuple[str | None, str | None]: | |
| 264 | + """Localisation publique (texte libre) → (région §7, ville) ou (None, None). | |
| 265 | + | |
| 266 | + Prudence maximale : ne retourne une région que si la localisation déclarée | |
| 267 | + est un lieu québécois reconnu sans ambiguïté. « Québec » seul = la province. | |
| 268 | + """ | |
| 269 | + if not location: | |
| 270 | + return None, None | |
| 271 | + loc = strip_accents(location).lower().strip() | |
| 272 | + # tronquer les précisions entre parenthèses SAUF « Québec (ville) » | |
| 273 | + base = re.sub(r"\s*\(.*?\)\s*", " ", loc).strip().rstrip(",") | |
| 274 | + if "quebec (ville)" in loc or base in ("ville de quebec",): | |
| 275 | + return "Capitale-Nationale", "Québec" | |
| 276 | + # retirer le suffixe province (« Blainville, Québec » → « blainville ») | |
| 277 | + base = re.sub(r",?\s*(quebec|qc|canada)\s*$", "", base).strip().rstrip(",") | |
| 278 | + if not base or base == "quebec": | |
| 279 | + return None, None # province seule → région inconnue | |
| 280 | + for city, region in _CITY_REGION.items(): | |
| 281 | + if base == city or base.startswith(city + ",") or base.endswith(" " + city): | |
| 282 | + return region, location.split(",")[0].split("(")[0].strip() | |
| 283 | + if base in {strip_accents(r).lower() for r in REGIONS}: | |
| 284 | + for r in REGIONS: | |
| 285 | + if strip_accents(r).lower() == base: | |
| 286 | + return r, None | |
| 287 | + return None, None | |
| 288 | + | |
| 289 | + | |
| 290 | +def parse_count(raw) -> int | None: | |
| 291 | + """« 1,2 M », « 12.3k », « 4 567 abonnés », 4567 → entier (None si inconnu).""" | |
| 292 | + if raw is None: | |
| 293 | + return None | |
| 294 | + if isinstance(raw, (int, float)): | |
| 295 | + return int(raw) | |
| 296 | + if str(raw).strip().startswith("@"): | |
| 297 | + return None # un handle (« @nom5b ») n'est pas un compteur | |
| 298 | + # frontières de mot obligatoires — sinon « tw5bu » se lirait « 5 milliards » | |
| 299 | + m = re.search(r"(?<![\w@.])([\d][\d\s .,]*)\s*([kKmMbB])?(?![\w])", str(raw)) | |
| 300 | + if not m: | |
| 301 | + return None | |
| 302 | + num = m.group(1).replace(" ", "").replace(" ", "") | |
| 303 | + # « 1.234.567 » / « 1,234,567 » = séparateurs de milliers ; « 1,2 »/« 1.2 » = décimal | |
| 304 | + if num.count(",") > 1 or (num.count(",") == 1 and len(num.split(",")[1]) == 3 | |
| 305 | + and not m.group(2)): | |
| 306 | + num = num.replace(",", "") | |
| 307 | + if num.count(".") > 1 or (num.count(".") == 1 and len(num.split(".")[1]) == 3 | |
| 308 | + and not m.group(2)): | |
| 309 | + num = num.replace(".", "") | |
| 310 | + num = num.replace(",", ".") | |
| 311 | + try: | |
| 312 | + value = float(num) | |
| 313 | + except ValueError: | |
| 314 | + return None | |
| 315 | + mult = {"k": 1e3, "m": 1e6, "b": 1e9}.get((m.group(2) or "").lower(), 1) | |
| 316 | + return int(value * mult) | |
| 317 | + | |
| 318 | + | |
| 319 | +def audience_tier(followers: int | None) -> str: | |
| 320 | + """Taille d'audience (§6.3), basée sur l'audience de la plateforme principale.""" | |
| 321 | + if not followers or followers <= 0: | |
| 322 | + return "nano" | |
| 323 | + if followers < 10_000: | |
| 324 | + return "nano" | |
| 325 | + if followers < 100_000: | |
| 326 | + return "micro" | |
| 327 | + if followers < 1_000_000: | |
| 328 | + return "macro" | |
| 329 | + return "mega" | |
| 330 | + | |
| 331 | + | |
| 332 | +def infer_creator_type(platforms: list[str], niche: str) -> str: | |
| 333 | + """Type de créateur par défaut selon sa plateforme dominante et sa niche.""" | |
| 334 | + first = platforms[0] if platforms else "" | |
| 335 | + if first == "youtube": | |
| 336 | + return "youtubeur" | |
| 337 | + if first in ("twitch", "kick"): | |
| 338 | + return "streamer" | |
| 339 | + if first == "tiktok": | |
| 340 | + return "createur-tiktok" | |
| 341 | + if first in ("substack", "site-web"): | |
| 342 | + return "createur-ecrit" | |
| 343 | + if first == "podcast": | |
| 344 | + return "podcasteur" | |
| 345 | + if niche == "humour": | |
| 346 | + return "humoriste" | |
| 347 | + if niche == "musique": | |
| 348 | + return "musicien" | |
| 349 | + return "influenceur" | |
| 350 | + | |
| 351 | + | |
| 352 | +def guess_language(*texts: str) -> str: | |
| 353 | + """Langue de création (§6.4) à partir des textes publics. Défaut : fr (Québec).""" | |
| 354 | + blob = strip_accents(" ".join(t for t in texts if t)).lower() | |
| 355 | + if not blob: | |
| 356 | + return "fr" | |
| 357 | + if re.search(r"\b(bilingue|bilingual|fr/en|en/fr)\b", blob): | |
| 358 | + return "bilingue" | |
| 359 | + if re.search(r"\b(contenu|content)\s+(quotidien\s+)?(en\s+)?anglais\b", blob) \ | |
| 360 | + or re.search(r"\benglish(-|\s)?(only|content|speaking)\b", blob): | |
| 361 | + return "en" | |
| 362 | + return "fr" | |
added
creaka/schema.py
+191 −0
@@ -0,0 +1,191 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/schema.py | |
| 4 | +# Desc: Modèle de données standardisé — Creator + PlatformAccount (CLAUDE.md §5) | |
| 5 | +# + normalisation centrale via finalize() | |
| 6 | +# ============================================================================== | |
| 7 | +"""Schéma standard d'une fiche créateur et de ses comptes de plateformes. | |
| 8 | + | |
| 9 | +Chaque connecteur, peu importe la source, doit produire des objets `Creator` | |
| 10 | +conformes. `finalize()` applique la couche commune (creaka/normalize.py) : | |
| 11 | +handles nettoyés, URLs canoniques, niches/région/langue/tier — les connecteurs | |
| 12 | +restent simples et remplissent les champs bruts. | |
| 13 | + | |
| 14 | +Validation stricte (CLAUDE.md §18) : un compte sans `url` ou sans `confidence` | |
| 15 | +est REJETÉ à la finalisation. | |
| 16 | +""" | |
| 17 | +from __future__ import annotations | |
| 18 | + | |
| 19 | +import hashlib | |
| 20 | +import json | |
| 21 | +from dataclasses import dataclass, field, asdict | |
| 22 | +from datetime import datetime, timezone | |
| 23 | + | |
| 24 | +from .normalize import ( | |
| 25 | + LANGUAGES, | |
| 26 | + NICHES, | |
| 27 | + PLATFORMS, | |
| 28 | + audience_tier, | |
| 29 | + canonical_url, | |
| 30 | + clean_bio, | |
| 31 | + clean_handle, | |
| 32 | + clean_name, | |
| 33 | + guess_language, | |
| 34 | + infer_creator_type, | |
| 35 | + map_niche, | |
| 36 | + normalize_platform, | |
| 37 | + slugify, | |
| 38 | +) | |
| 39 | + | |
| 40 | +__all__ = ["Creator", "PlatformAccount", "now_iso"] | |
| 41 | + | |
| 42 | + | |
| 43 | +def now_iso() -> str: | |
| 44 | + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") | |
| 45 | + | |
| 46 | + | |
| 47 | +@dataclass | |
| 48 | +class PlatformAccount: | |
| 49 | + """Un compte public d'un créateur sur UNE plateforme (§5).""" | |
| 50 | + | |
| 51 | + platform: str # * canonique §6.2 | |
| 52 | + handle: str # * sans @, minuscules | |
| 53 | + url: str = "" # * forme canonique | |
| 54 | + followers: int | None = None # abonnés (None = inconnu) | |
| 55 | + verified: bool | None = None # badge vérifié (None = inconnu) | |
| 56 | + confidence: float | None = None # * confiance du rattachement (§12) | |
| 57 | + last_checked: str = "" # * ISO-8601 | |
| 58 | + signal: str = "" # signal d'origine du rattachement (§12.1) | |
| 59 | + metrics: dict = field(default_factory=dict) # métriques étendues publiques : | |
| 60 | + # following, posts, likes, views, videos, category… (selon la plateforme) | |
| 61 | + | |
| 62 | + def finalize(self) -> "PlatformAccount": | |
| 63 | + self.platform = normalize_platform(self.platform) | |
| 64 | + self.handle = clean_handle(self.handle) | |
| 65 | + self.url = canonical_url(self.platform, self.handle, fallback=self.url) | |
| 66 | + if not self.handle: | |
| 67 | + raise ValueError("compte sans handle") | |
| 68 | + if not self.url: | |
| 69 | + raise ValueError(f"compte {self.platform}:{self.handle} sans url") | |
| 70 | + if self.confidence is None: | |
| 71 | + raise ValueError(f"compte {self.platform}:{self.handle} sans confidence") | |
| 72 | + self.confidence = max(0.0, min(1.0, float(self.confidence))) | |
| 73 | + if self.followers is not None: | |
| 74 | + self.followers = max(0, int(self.followers)) | |
| 75 | + if not self.last_checked: | |
| 76 | + self.last_checked = now_iso() | |
| 77 | + return self | |
| 78 | + | |
| 79 | + @property | |
| 80 | + def key(self) -> str: | |
| 81 | + """Clé d'unicité d'un compte : plateforme + handle normalisé.""" | |
| 82 | + return f"{self.platform}:{self.handle}" | |
| 83 | + | |
| 84 | + | |
| 85 | +@dataclass | |
| 86 | +class Creator: | |
| 87 | + """Fiche créateur canonique Créa-Ka (CLAUDE.md §5).""" | |
| 88 | + | |
| 89 | + source: str # id de la source (registre data/sources.json) | |
| 90 | + external_id: str # identifiant chez la source | |
| 91 | + display_name: str # * nom public / nom de scène | |
| 92 | + legal_name: str | None = None # SEULEMENT si rendu public par le créateur | |
| 93 | + bio: str = "" | |
| 94 | + niches: list[str] = field(default_factory=list) | |
| 95 | + creator_type: str = "" | |
| 96 | + region: str | None = None # une des 17 régions §7, sinon None | |
| 97 | + city: str | None = None | |
| 98 | + languages: list[str] = field(default_factory=list) | |
| 99 | + primary_platform: str = "" | |
| 100 | + platforms: list[PlatformAccount] = field(default_factory=list) | |
| 101 | + link_in_bio_url: str | None = None | |
| 102 | + avatar_url: str | None = None # photo de profil PUBLIQUE (URL source) | |
| 103 | + total_reach: int | None = None | |
| 104 | + audience_tier: str = "" | |
| 105 | + business_contact: str | None = None # courriel PRO affiché publiquement (§15) | |
| 106 | + agency: str | None = None | |
| 107 | + is_minor: bool = False | |
| 108 | + status: str = "active" # active | inactive | opted_out | |
| 109 | + notes: str = "" # contexte public (listes médias), affichable | |
| 110 | + source_ids: list[str] = field(default_factory=list) | |
| 111 | + | |
| 112 | + @property | |
| 113 | + def uid(self) -> str: | |
| 114 | + return f"{self.source}:{self.external_id}" | |
| 115 | + | |
| 116 | + def content_hash(self) -> str: | |
| 117 | + """Hash du contenu pour la détection de changements.""" | |
| 118 | + payload = asdict(self) | |
| 119 | + for acc in payload["platforms"]: | |
| 120 | + acc.pop("last_checked", None) # horodatage ≠ changement de contenu | |
| 121 | + blob = json.dumps(payload, sort_keys=True, ensure_ascii=False) | |
| 122 | + return hashlib.sha256(blob.encode("utf-8")).hexdigest() | |
| 123 | + | |
| 124 | + def finalize(self) -> "Creator": | |
| 125 | + """Applique la normalisation commune. Appelé par le pipeline d'ingestion. | |
| 126 | + | |
| 127 | + Idempotent ; ne remplace jamais une valeur explicite du connecteur. | |
| 128 | + Rejette la fiche si aucun compte valide (un créateur sans lien n'a pas | |
| 129 | + sa place dans un annuaire de liens). | |
| 130 | + """ | |
| 131 | + self.display_name = clean_name(self.display_name) | |
| 132 | + if not self.display_name: | |
| 133 | + raise ValueError("créateur sans nom d'affichage") | |
| 134 | + if not self.external_id: | |
| 135 | + self.external_id = slugify(self.display_name) | |
| 136 | + self.bio = clean_bio(self.bio) | |
| 137 | + | |
| 138 | + # comptes : normaliser, valider, dédupliquer (meilleure confiance gagne) | |
| 139 | + seen: dict[str, PlatformAccount] = {} | |
| 140 | + for acc in self.platforms: | |
| 141 | + try: | |
| 142 | + acc = acc.finalize() | |
| 143 | + except ValueError: | |
| 144 | + continue # un compte invalide ne bloque pas la fiche (§18 : rejeté) | |
| 145 | + prev = seen.get(acc.key) | |
| 146 | + if prev is None or (acc.confidence or 0) > (prev.confidence or 0): | |
| 147 | + if prev is not None and prev.followers and not acc.followers: | |
| 148 | + acc.followers = prev.followers | |
| 149 | + if prev is not None: | |
| 150 | + acc.metrics = {**prev.metrics, **acc.metrics} | |
| 151 | + seen[acc.key] = acc | |
| 152 | + # tri : abonnés décroissants (None en dernier), pour primary_platform | |
| 153 | + self.platforms = sorted( | |
| 154 | + seen.values(), key=lambda a: (a.followers is None, -(a.followers or 0))) | |
| 155 | + if not self.platforms: | |
| 156 | + raise ValueError(f"{self.display_name}: aucun compte valide") | |
| 157 | + | |
| 158 | + self.niches = [map_niche(n) for n in (self.niches or ["autre"])] | |
| 159 | + self.niches = sorted({n for n in self.niches if n in NICHES}) or ["autre"] | |
| 160 | + | |
| 161 | + if not self.primary_platform: | |
| 162 | + self.primary_platform = self.platforms[0].platform | |
| 163 | + self.primary_platform = normalize_platform(self.primary_platform) | |
| 164 | + if self.primary_platform not in PLATFORMS: | |
| 165 | + self.primary_platform = self.platforms[0].platform | |
| 166 | + | |
| 167 | + if not self.languages: | |
| 168 | + self.languages = [guess_language(self.bio, self.notes)] | |
| 169 | + self.languages = [l for l in self.languages if l in LANGUAGES] or ["fr"] | |
| 170 | + | |
| 171 | + if not self.creator_type: | |
| 172 | + self.creator_type = infer_creator_type( | |
| 173 | + [a.platform for a in self.platforms], self.niches[0]) | |
| 174 | + | |
| 175 | + # portée totale : somme indicative des audiences connues | |
| 176 | + known = [a.followers for a in self.platforms if a.followers] | |
| 177 | + self.total_reach = sum(known) if known else None | |
| 178 | + primary = next((a.followers for a in self.platforms | |
| 179 | + if a.platform == self.primary_platform and a.followers), None) | |
| 180 | + if not self.audience_tier: | |
| 181 | + self.audience_tier = audience_tier(primary or (max(known) if known else None)) | |
| 182 | + | |
| 183 | + if not self.source_ids: | |
| 184 | + self.source_ids = [self.uid] | |
| 185 | + | |
| 186 | + # régime restreint pour les mineurs (§15) — appliqué aussi dans ethics.py | |
| 187 | + if self.is_minor: | |
| 188 | + self.business_contact = None | |
| 189 | + self.city = None | |
| 190 | + self.region = None | |
| 191 | + return self | |
added
creaka/web.py
+130 −0
@@ -0,0 +1,130 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: creaka/web.py | |
| 4 | +# Desc: API FastAPI (JSON) + service du frontend statique (frontend/dist) — | |
| 5 | +# annuaire cherchable par niche, région, langue, plateforme, taille | |
| 6 | +# ============================================================================== | |
| 7 | +from __future__ import annotations | |
| 8 | + | |
| 9 | +import json | |
| 10 | +from pathlib import Path | |
| 11 | + | |
| 12 | +from fastapi import FastAPI, HTTPException, Query | |
| 13 | +from fastapi.middleware.cors import CORSMiddleware | |
| 14 | +from fastapi.middleware.gzip import GZipMiddleware | |
| 15 | +from fastapi.responses import FileResponse | |
| 16 | +from pydantic import BaseModel, Field | |
| 17 | + | |
| 18 | +from . import db, ethics | |
| 19 | +from .normalize import NICHES, PLATFORMS, REGIONS | |
| 20 | + | |
| 21 | +ROOT = Path(__file__).resolve().parent.parent | |
| 22 | +FRONTEND_DIST = ROOT / "frontend" / "dist" | |
| 23 | +SOURCES_PATH = ROOT / "data" / "sources.json" | |
| 24 | + | |
| 25 | +app = FastAPI(title="Créa-Ka API", version="1.0", | |
| 26 | + description="Annuaire public des créateurs de contenu québécois " | |
| 27 | + "— tous leurs comptes, toutes leurs plateformes.") | |
| 28 | +app.add_middleware(CORSMiddleware, allow_origins=["*"], | |
| 29 | + allow_methods=["*"], allow_headers=["*"]) | |
| 30 | +app.add_middleware(GZipMiddleware, minimum_size=1000) | |
| 31 | + | |
| 32 | +_con = db.connect() | |
| 33 | + | |
| 34 | + | |
| 35 | +@app.get("/healthz") | |
| 36 | +def healthz(): | |
| 37 | + return {"ok": True} | |
| 38 | + | |
| 39 | + | |
| 40 | +@app.get("/api/health") | |
| 41 | +def health(): | |
| 42 | + s = db.stats(_con) | |
| 43 | + return {"ok": True, "creators": s["creators"], "accounts": s["accounts"], | |
| 44 | + "last_sync": s["last_sync"]} | |
| 45 | + | |
| 46 | + | |
| 47 | +@app.get("/api/creators") | |
| 48 | +def list_creators(q: str = "", niche: str = "", region: str = "", | |
| 49 | + langue: str = "", plateforme: str = "", tier: str = "", | |
| 50 | + sort: str = "reach", | |
| 51 | + limit: int = Query(60, le=200), offset: int = Query(0, ge=0)): | |
| 52 | + return db.search(_con, q=q.strip(), niche=niche, region=region, | |
| 53 | + langue=langue, plateforme=plateforme, tier=tier, | |
| 54 | + sort=sort, limit=limit, offset=offset) | |
| 55 | + | |
| 56 | + | |
| 57 | +@app.get("/api/creators/{cid}") | |
| 58 | +def get_creator(cid: str): | |
| 59 | + creator = db.get_creator(_con, cid) | |
| 60 | + if creator is None: | |
| 61 | + raise HTTPException(404, "créateur introuvable") | |
| 62 | + return creator | |
| 63 | + | |
| 64 | + | |
| 65 | +@app.get("/api/stats") | |
| 66 | +def get_stats(): | |
| 67 | + return db.stats(_con) | |
| 68 | + | |
| 69 | + | |
| 70 | +@app.get("/api/taxonomies") | |
| 71 | +def taxonomies(): | |
| 72 | + return {"niches": sorted(NICHES), "plateformes": sorted(PLATFORMS), | |
| 73 | + "regions": sorted(REGIONS), | |
| 74 | + "tiers": ["nano", "micro", "macro", "mega"], | |
| 75 | + "langues": ["fr", "en", "bilingue"]} | |
| 76 | + | |
| 77 | + | |
| 78 | +@app.get("/api/sources") | |
| 79 | +def sources(): | |
| 80 | + if SOURCES_PATH.exists(): | |
| 81 | + return json.loads(SOURCES_PATH.read_text(encoding="utf-8")) | |
| 82 | + return {"sources": []} | |
| 83 | + | |
| 84 | + | |
| 85 | +class OptOutRequest(BaseModel): | |
| 86 | + """Demande de retrait (§15 — opt-out obligatoire et respecté).""" | |
| 87 | + name: str = Field("", max_length=200) | |
| 88 | + account: str = Field("", max_length=200, | |
| 89 | + description="plateforme:handle, ex. instagram:monnom") | |
| 90 | + reason: str = Field("", max_length=500) | |
| 91 | + contact: str = Field("", max_length=200) | |
| 92 | + | |
| 93 | + | |
| 94 | +@app.post("/api/optout") | |
| 95 | +def optout(req: OptOutRequest): | |
| 96 | + """Retrait immédiat : la fiche est masquée et exclue des ré-agrégations. | |
| 97 | + | |
| 98 | + La vie privée prime sur la complétude (§15) : on masque d'abord, la | |
| 99 | + demande est journalisée pour vérification humaine ensuite. | |
| 100 | + """ | |
| 101 | + name = req.name.strip() or None | |
| 102 | + acc = req.account.strip().lower() or None | |
| 103 | + if not name and not acc: | |
| 104 | + raise HTTPException(400, "préciser un nom ou un compte (plateforme:handle)") | |
| 105 | + ethics.add_optout(name=name, account=acc, reason=req.reason, | |
| 106 | + contact=req.contact) | |
| 107 | + masked = db.apply_optout(_con, name=name, account=acc) | |
| 108 | + return {"ok": True, "masked": masked, | |
| 109 | + "message": "Fiche masquée. La demande sera vérifiée ; merci."} | |
| 110 | + | |
| 111 | + | |
| 112 | +# --- frontend statique ----------------------------------------------------------- | |
| 113 | + | |
| 114 | +@app.get("/") | |
| 115 | +def index(): | |
| 116 | + idx = FRONTEND_DIST / "index.html" | |
| 117 | + if idx.exists(): | |
| 118 | + return FileResponse(idx) | |
| 119 | + raise HTTPException(404, "frontend non construit") | |
| 120 | + | |
| 121 | + | |
| 122 | +@app.get("/{path:path}") | |
| 123 | +def static_files(path: str): | |
| 124 | + target = (FRONTEND_DIST / path).resolve() | |
| 125 | + if target.is_file() and str(target).startswith(str(FRONTEND_DIST.resolve())): | |
| 126 | + return FileResponse(target) | |
| 127 | + idx = FRONTEND_DIST / "index.html" | |
| 128 | + if idx.exists(): | |
| 129 | + return FileResponse(idx) # SPA fallback | |
| 130 | + raise HTTPException(404, "introuvable") | |
added
data/optout.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "_author": "Simon-Pierre Boucher <contact@spboucher.ai>", | |
| 3 | + "_file": "data/optout.json", | |
| 4 | + "_desc": "Liste de suppression persistante (opt-out §15) — fiches masquées et exclues des ré-agrégations futures", | |
| 5 | + "names": [], | |
| 6 | + "accounts": [] | |
| 7 | +} | |
added
data/seed/qc_influenceurs.json
+10732 −0
@@ -0,0 +1,10732 @@ | ||
| 1 | +[ | |
| 2 | + { | |
| 3 | + "name": "Enola Bédard", | |
| 4 | + "handles": { | |
| 5 | + "instagram": "enola.bedard", | |
| 6 | + "tiktok": "enola.bedard", | |
| 7 | + "youtube": "enola.bedard", | |
| 8 | + "facebook": null, | |
| 9 | + "x": null, | |
| 10 | + "twitch": null | |
| 11 | + }, | |
| 12 | + "metrics": { | |
| 13 | + "instagram_followers": 2600000, | |
| 14 | + "tiktok_followers": 17000000, | |
| 15 | + "youtube_subscribers": 4900000, | |
| 16 | + "facebook_followers": 1100000, | |
| 17 | + "x_followers": null, | |
| 18 | + "twitch_followers": null | |
| 19 | + }, | |
| 20 | + "category": "danse", | |
| 21 | + "location": "Los Angeles (originaire du Québec)", | |
| 22 | + "notes": "danseuse TikTok originaire de la ville de Québec | Québécoise la plus suivie sur TikTok grâce à la danse; contenu quotidien en anglais, portée internationale. Chiffres ~2021-2022. | Danseuse/chorégraphe virale sur TikTok pendant la pandémie; #1 des tops frank, metricool et Favikon Québec (24,4M abonnés cumulés selon Favikon). Aussi actrice et chanteuse.", | |
| 23 | + "sources": [ | |
| 24 | + "billie", | |
| 25 | + "alexturcotte_tiktok", | |
| 26 | + "frank", | |
| 27 | + "metricool", | |
| 28 | + "canadult", | |
| 29 | + "favikon_qc" | |
| 30 | + ], | |
| 31 | + "total_followers": 25600000, | |
| 32 | + "platform_count": 4, | |
| 33 | + "tier": "méga (1M+)" | |
| 34 | + }, | |
| 35 | + { | |
| 36 | + "name": "Céline Dion", | |
| 37 | + "handles": { | |
| 38 | + "instagram": null, | |
| 39 | + "tiktok": "celinedion", | |
| 40 | + "youtube": "celinedion", | |
| 41 | + "facebook": null, | |
| 42 | + "x": null, | |
| 43 | + "twitch": null | |
| 44 | + }, | |
| 45 | + "metrics": { | |
| 46 | + "instagram_followers": 4200000, | |
| 47 | + "tiktok_followers": 5800000, | |
| 48 | + "youtube_subscribers": 10200000, | |
| 49 | + "facebook_followers": null, | |
| 50 | + "x_followers": null, | |
| 51 | + "twitch_followers": null | |
| 52 | + }, | |
| 53 | + "category": "musique", | |
| 54 | + "location": "Québec", | |
| 55 | + "notes": "Mention comparative dans l'article (4.2M sur Instagram vs Enola Bédard). Chiffres ~2021-2022. | enrichi via recherche web 2026-08", | |
| 56 | + "sources": [ | |
| 57 | + "alexturcotte_tiktok", | |
| 58 | + "firecrawl_enrich" | |
| 59 | + ], | |
| 60 | + "total_followers": 20200000, | |
| 61 | + "platform_count": 3, | |
| 62 | + "tier": "méga (1M+)" | |
| 63 | + }, | |
| 64 | + { | |
| 65 | + "name": "Sarah-Jade Bleau", | |
| 66 | + "handles": { | |
| 67 | + "instagram": "sj.bleau", | |
| 68 | + "tiktok": "sjbleau", | |
| 69 | + "youtube": "sjbleau", | |
| 70 | + "facebook": null, | |
| 71 | + "x": null, | |
| 72 | + "twitch": null | |
| 73 | + }, | |
| 74 | + "metrics": { | |
| 75 | + "instagram_followers": 1600000, | |
| 76 | + "tiktok_followers": 14600000, | |
| 77 | + "youtube_subscribers": 38200, | |
| 78 | + "facebook_followers": null, | |
| 79 | + "x_followers": null, | |
| 80 | + "twitch_followers": null | |
| 81 | + }, | |
| 82 | + "category": "danse", | |
| 83 | + "location": "Québec", | |
| 84 | + "notes": "carrière explosée sur TikTok en 2020 grâce à la danse | Vidéos virales amusantes, 2e Québécoise la plus suivie sur TikTok. Chiffres ~2021-2022. | Chorégraphies robotiques ultra précises sur TikTok (aussi @sjbleauellington); marque mode/beauté Champagnemami; partenaire L'Oréal Paris. 16,3M abonnés cumulés selon Favikon.", | |
| 85 | + "sources": [ | |
| 86 | + "billie", | |
| 87 | + "alexturcotte_tiktok", | |
| 88 | + "frank", | |
| 89 | + "metricool", | |
| 90 | + "canadult", | |
| 91 | + "favikon_qc" | |
| 92 | + ], | |
| 93 | + "total_followers": 16238200, | |
| 94 | + "platform_count": 3, | |
| 95 | + "tier": "méga (1M+)" | |
| 96 | + }, | |
| 97 | + { | |
| 98 | + "name": "Jordy Boulet-Viau", | |
| 99 | + "handles": { | |
| 100 | + "instagram": "jordybouletviau", | |
| 101 | + "tiktok": null, | |
| 102 | + "youtube": null, | |
| 103 | + "facebook": null, | |
| 104 | + "x": null, | |
| 105 | + "twitch": null | |
| 106 | + }, | |
| 107 | + "metrics": { | |
| 108 | + "instagram_followers": 409700, | |
| 109 | + "tiktok_followers": 6300000, | |
| 110 | + "youtube_subscribers": 991000, | |
| 111 | + "facebook_followers": 177000, | |
| 112 | + "x_followers": null, | |
| 113 | + "twitch_followers": null | |
| 114 | + }, | |
| 115 | + "category": "humour", | |
| 116 | + "location": "Montréal", | |
| 117 | + "notes": "storytelling authentique, humour et santé mentale; 170K sur Snapchat", | |
| 118 | + "sources": [ | |
| 119 | + "collabstr" | |
| 120 | + ], | |
| 121 | + "total_followers": 7877700, | |
| 122 | + "platform_count": 4, | |
| 123 | + "tier": "méga (1M+)" | |
| 124 | + }, | |
| 125 | + { | |
| 126 | + "name": "Shina Nova", | |
| 127 | + "handles": { | |
| 128 | + "instagram": "shinanova", | |
| 129 | + "tiktok": "shinanova", | |
| 130 | + "youtube": "shinanovayt", | |
| 131 | + "facebook": null, | |
| 132 | + "x": null, | |
| 133 | + "twitch": null | |
| 134 | + }, | |
| 135 | + "metrics": { | |
| 136 | + "instagram_followers": 2000000, | |
| 137 | + "tiktok_followers": 4400000, | |
| 138 | + "youtube_subscribers": 254000, | |
| 139 | + "facebook_followers": 415000, | |
| 140 | + "x_followers": null, | |
| 141 | + "twitch_followers": null | |
| 142 | + }, | |
| 143 | + "category": "musique", | |
| 144 | + "location": "Montréal", | |
| 145 | + "notes": "no 1 du palmarès Billie; activiste et TikTokeuse inuite (chant de gorge) | Créatrice inuite, chant de gorge en duo avec sa mère; valorise la culture inuite et la santé mentale. #2 du top Favikon Québec (6,6M abonnés cumulés).", | |
| 146 | + "sources": [ | |
| 147 | + "billie", | |
| 148 | + "frank", | |
| 149 | + "metricool", | |
| 150 | + "favikon_qc" | |
| 151 | + ], | |
| 152 | + "total_followers": 7069000, | |
| 153 | + "platform_count": 4, | |
| 154 | + "tier": "méga (1M+)" | |
| 155 | + }, | |
| 156 | + { | |
| 157 | + "name": "Marc Fitt", | |
| 158 | + "handles": { | |
| 159 | + "instagram": "marc_fitt", | |
| 160 | + "tiktok": null, | |
| 161 | + "youtube": "marc fitt", | |
| 162 | + "facebook": "marcfitt", | |
| 163 | + "x": null, | |
| 164 | + "twitch": null | |
| 165 | + }, | |
| 166 | + "metrics": { | |
| 167 | + "instagram_followers": 1200000, | |
| 168 | + "tiktok_followers": null, | |
| 169 | + "youtube_subscribers": 311000, | |
| 170 | + "facebook_followers": 4100000, | |
| 171 | + "x_followers": null, | |
| 172 | + "twitch_followers": null | |
| 173 | + }, | |
| 174 | + "category": "fitness", | |
| 175 | + "location": "Montréal", | |
| 176 | + "notes": "influenceur fitness, créateur de la marque Rise | Entraînement et fitness; 1M d'abonnés Instagram. Chiffres YouTube 2022. | Un des influenceurs fitness les plus suivis au Canada; programmes d'entraînement et podcast Fitt Talk. | enrichi via recherche web 2026-08; facebook = likes de page", | |
| 177 | + "sources": [ | |
| 178 | + "billie", | |
| 179 | + "billie_youtube", | |
| 180 | + "frank", | |
| 181 | + "firecrawl_enrich" | |
| 182 | + ], | |
| 183 | + "total_followers": 5611000, | |
| 184 | + "platform_count": 3, | |
| 185 | + "tier": "méga (1M+)" | |
| 186 | + }, | |
| 187 | + { | |
| 188 | + "name": "Adam Paradisio (ADAM)", | |
| 189 | + "handles": { | |
| 190 | + "instagram": "adam_de_youtube", | |
| 191 | + "tiktok": "adamparadisio", | |
| 192 | + "youtube": "adamparadisio", | |
| 193 | + "facebook": null, | |
| 194 | + "x": null, | |
| 195 | + "twitch": null | |
| 196 | + }, | |
| 197 | + "metrics": { | |
| 198 | + "instagram_followers": null, | |
| 199 | + "tiktok_followers": 3700000, | |
| 200 | + "youtube_subscribers": 1855415, | |
| 201 | + "facebook_followers": null, | |
| 202 | + "x_followers": null, | |
| 203 | + "twitch_followers": null | |
| 204 | + }, | |
| 205 | + "category": "humour", | |
| 206 | + "location": "Québec", | |
| 207 | + "notes": "Gagnant Gala InfluenceCréation 2025 — Youtubeur de l'année et Vidéo de l'année, long format. Défis, pranks et vidéos de survie; 3,1M abonnés cumulés selon Favikon (#3). | enrichi via recherche web 2026-08", | |
| 208 | + "sources": [ | |
| 209 | + "billie_gala", | |
| 210 | + "grenier_gala", | |
| 211 | + "favikon_qc", | |
| 212 | + "firecrawl_enrich" | |
| 213 | + ], | |
| 214 | + "total_followers": 5555415, | |
| 215 | + "platform_count": 3, | |
| 216 | + "tier": "méga (1M+)" | |
| 217 | + }, | |
| 218 | + { | |
| 219 | + "name": "Denyzee", | |
| 220 | + "handles": { | |
| 221 | + "instagram": "denyzee_", | |
| 222 | + "tiktok": "denyzee", | |
| 223 | + "youtube": "denyzee", | |
| 224 | + "facebook": null, | |
| 225 | + "x": null, | |
| 226 | + "twitch": null | |
| 227 | + }, | |
| 228 | + "metrics": { | |
| 229 | + "instagram_followers": 443000, | |
| 230 | + "tiktok_followers": 898300, | |
| 231 | + "youtube_subscribers": 2600000, | |
| 232 | + "facebook_followers": 582000, | |
| 233 | + "x_followers": null, | |
| 234 | + "twitch_followers": null | |
| 235 | + }, | |
| 236 | + "category": "humour", | |
| 237 | + "location": "Québec (originaire de France)", | |
| 238 | + "notes": "Vidéos humoristiques; retirée du classement Titan 2022 car s'identifie désormais en France. Chiffres 2022. | Figure incontournable du YouTube québécois; sketchs, expériences sociales et comparaisons France/Québec; installée au Canada depuis 2013. | enrichi via recherche web 2026-08", | |
| 239 | + "sources": [ | |
| 240 | + "billie_youtube", | |
| 241 | + "titan_youtube_2022", | |
| 242 | + "metricool", | |
| 243 | + "firecrawl_enrich" | |
| 244 | + ], | |
| 245 | + "total_followers": 4523300, | |
| 246 | + "platform_count": 4, | |
| 247 | + "tier": "méga (1M+)" | |
| 248 | + }, | |
| 249 | + { | |
| 250 | + "name": "Laurent Dagenais", | |
| 251 | + "handles": { | |
| 252 | + "instagram": "laurent.dagenais", | |
| 253 | + "tiktok": "laurent.dagenais", | |
| 254 | + "youtube": "laurentdagenais", | |
| 255 | + "facebook": "cheflaurentdagenais", | |
| 256 | + "x": null, | |
| 257 | + "twitch": null | |
| 258 | + }, | |
| 259 | + "metrics": { | |
| 260 | + "instagram_followers": 1900000, | |
| 261 | + "tiktok_followers": 2000000, | |
| 262 | + "youtube_subscribers": 79200, | |
| 263 | + "facebook_followers": 88000, | |
| 264 | + "x_followers": null, | |
| 265 | + "twitch_followers": null | |
| 266 | + }, | |
| 267 | + "category": "food", | |
| 268 | + "location": "Montréal", | |
| 269 | + "notes": "Chef cuisinier de renom, recettes à portée internationale. Chiffres ~2021-2022. | Chef viral sur TikTok, livre Toujours Faim (2022), série Always Hungry. Gagnant Gala InfluenceCréation 2025 — Créateur de l'année, rayonnement international. | Chef #1 du top food Feedspot Québec (Mega); @toujourssoif, restaurant Limousine; 2M+ TikTok selon La Grappe", | |
| 270 | + "sources": [ | |
| 271 | + "alexturcotte_tiktok", | |
| 272 | + "frank", | |
| 273 | + "metricool", | |
| 274 | + "canadult", | |
| 275 | + "billie_gala", | |
| 276 | + "grenier_gala", | |
| 277 | + "feedspot_food", | |
| 278 | + "lagrappe_food" | |
| 279 | + ], | |
| 280 | + "total_followers": 4067200, | |
| 281 | + "platform_count": 4, | |
| 282 | + "tier": "méga (1M+)" | |
| 283 | + }, | |
| 284 | + { | |
| 285 | + "name": "Rachid Badouri", | |
| 286 | + "handles": { | |
| 287 | + "instagram": "rachidbadouri", | |
| 288 | + "tiktok": "rachidbadouriofficiel", | |
| 289 | + "youtube": "rachidbadouriofficiel", | |
| 290 | + "facebook": null, | |
| 291 | + "x": null, | |
| 292 | + "twitch": null | |
| 293 | + }, | |
| 294 | + "metrics": { | |
| 295 | + "instagram_followers": 542000, | |
| 296 | + "tiktok_followers": 2600000, | |
| 297 | + "youtube_subscribers": null, | |
| 298 | + "facebook_followers": null, | |
| 299 | + "x_followers": null, | |
| 300 | + "twitch_followers": null | |
| 301 | + }, | |
| 302 | + "category": "humour", | |
| 303 | + "location": "Montréal", | |
| 304 | + "notes": "Humoriste, vedette québécoise la plus connue hors TikTok sur la plateforme. Chiffres ~2021-2022. | #1 humoriste QC sur les réseaux: 4 031 400 abonnés cumulés (FB+IG+TikTok+X, juin 2024); 2,6M sur TikTok seulement | enrichi via recherche web 2026-08", | |
| 305 | + "sources": [ | |
| 306 | + "alexturcotte_tiktok", | |
| 307 | + "jdm_humoristes", | |
| 308 | + "firecrawl_enrich" | |
| 309 | + ], | |
| 310 | + "total_followers": 3142000, | |
| 311 | + "platform_count": 3, | |
| 312 | + "tier": "méga (1M+)" | |
| 313 | + }, | |
| 314 | + { | |
| 315 | + "name": "Polo (CB Games)", | |
| 316 | + "handles": { | |
| 317 | + "instagram": "polobleu_officiel", | |
| 318 | + "tiktok": null, | |
| 319 | + "youtube": "cbgamespolo", | |
| 320 | + "facebook": null, | |
| 321 | + "x": null, | |
| 322 | + "twitch": null | |
| 323 | + }, | |
| 324 | + "metrics": { | |
| 325 | + "instagram_followers": 170000, | |
| 326 | + "tiktok_followers": null, | |
| 327 | + "youtube_subscribers": 2890000, | |
| 328 | + "facebook_followers": null, | |
| 329 | + "x_followers": null, | |
| 330 | + "twitch_followers": null | |
| 331 | + }, | |
| 332 | + "category": "gaming", | |
| 333 | + "location": "Québec", | |
| 334 | + "notes": "Gamer à l'énergie contagieuse; YouTubeur QC #1 avec le plus d'abonnés et #1 du top 100 Titan (depuis 2018). Chiffres 2022. | YouTubeur gaming (Fortnite, Pokémon, Minecraft), contenu bilingue FR/EN, aussi vie de père. 2,9M abonnés cumulés selon Favikon (#16). | enrichi via recherche web 2026-08", | |
| 335 | + "sources": [ | |
| 336 | + "billie_youtube", | |
| 337 | + "titan_youtube_2022", | |
| 338 | + "favikon_qc", | |
| 339 | + "firecrawl_enrich" | |
| 340 | + ], | |
| 341 | + "total_followers": 3060000, | |
| 342 | + "platform_count": 2, | |
| 343 | + "tier": "méga (1M+)" | |
| 344 | + }, | |
| 345 | + { | |
| 346 | + "name": "Willibed", | |
| 347 | + "handles": { | |
| 348 | + "instagram": "willibed", | |
| 349 | + "tiktok": "willibed", | |
| 350 | + "youtube": null, | |
| 351 | + "facebook": null, | |
| 352 | + "x": "willibed", | |
| 353 | + "twitch": null | |
| 354 | + }, | |
| 355 | + "metrics": { | |
| 356 | + "instagram_followers": 76000, | |
| 357 | + "tiktok_followers": 1600000, | |
| 358 | + "youtube_subscribers": 1000000, | |
| 359 | + "facebook_followers": null, | |
| 360 | + "x_followers": null, | |
| 361 | + "twitch_followers": null | |
| 362 | + }, | |
| 363 | + "category": "humour", | |
| 364 | + "location": "Québec", | |
| 365 | + "notes": "Vidéos insolites et fun facts étranges. Listé deux fois (4e et 6e) dans l'article. Chiffres ~2021-2022. | YouTubeur de 21 ans, défis extrêmes et non conventionnels depuis 2013 (100K+ abonnés YouTube); collabs Sports Experts et Couche-Tard. 3M abonnés cumulés selon Favikon (#15). | enrichi via recherche web 2026-08; YouTube 1M+ selon post X", | |
| 366 | + "sources": [ | |
| 367 | + "alexturcotte_tiktok", | |
| 368 | + "favikon_qc", | |
| 369 | + "firecrawl_enrich" | |
| 370 | + ], | |
| 371 | + "total_followers": 2676000, | |
| 372 | + "platform_count": 4, | |
| 373 | + "tier": "méga (1M+)" | |
| 374 | + }, | |
| 375 | + { | |
| 376 | + "name": "Cocotte (Arianne Vallières)", | |
| 377 | + "handles": { | |
| 378 | + "instagram": "cocottee._", | |
| 379 | + "tiktok": null, | |
| 380 | + "youtube": "cocotte", | |
| 381 | + "facebook": null, | |
| 382 | + "x": null, | |
| 383 | + "twitch": "cocotte" | |
| 384 | + }, | |
| 385 | + "metrics": { | |
| 386 | + "instagram_followers": 1200000, | |
| 387 | + "tiktok_followers": null, | |
| 388 | + "youtube_subscribers": 1360000, | |
| 389 | + "facebook_followers": null, | |
| 390 | + "x_followers": null, | |
| 391 | + "twitch_followers": null | |
| 392 | + }, | |
| 393 | + "category": "gaming", | |
| 394 | + "location": "Québec", | |
| 395 | + "notes": "Streameuse Twitch/YouTube, millions d'abonnés cumulés; participation au GP Explorer. Top YouTubeurs QC 2026 (ONEWAY). | Streameuse Twitch à plein temps (547,4K abonnés Twitch); gagnante Gala InfluenceCréation 2025 — Streamer de l'année. #5 Favikon Québec (3,1M cumulés). | Streameuse gaming/Just Chatting, ex-pompière; large communauté en France, a participé au GP Explorer de Squeezie | enrichi via recherche web 2026-08", | |
| 396 | + "sources": [ | |
| 397 | + "oneway_youtube", | |
| 398 | + "metricool", | |
| 399 | + "billie_gala", | |
| 400 | + "grenier_gala", | |
| 401 | + "favikon_qc", | |
| 402 | + "leregard_twitch", | |
| 403 | + "firecrawl_enrich" | |
| 404 | + ], | |
| 405 | + "total_followers": 2560000, | |
| 406 | + "platform_count": 3, | |
| 407 | + "tier": "méga (1M+)" | |
| 408 | + }, | |
| 409 | + { | |
| 410 | + "name": "tai_tl", | |
| 411 | + "handles": { | |
| 412 | + "instagram": "tai_tl", | |
| 413 | + "tiktok": "tai_tl", | |
| 414 | + "youtube": null, | |
| 415 | + "facebook": null, | |
| 416 | + "x": null, | |
| 417 | + "twitch": null | |
| 418 | + }, | |
| 419 | + "metrics": { | |
| 420 | + "instagram_followers": 828000, | |
| 421 | + "tiktok_followers": 1600000, | |
| 422 | + "youtube_subscribers": null, | |
| 423 | + "facebook_followers": null, | |
| 424 | + "x_followers": null, | |
| 425 | + "twitch_followers": null | |
| 426 | + }, | |
| 427 | + "category": "humour", | |
| 428 | + "location": "Québec", | |
| 429 | + "notes": "Créatrice TikTok (1,1M+ abonnés TikTok): lip-sync, danse, humour; spectacles de comédie (Casino de Paris). 2,7M abonnés cumulés selon Favikon (#11). | enrichi via recherche web 2026-08; Tailaire Laguerre", | |
| 430 | + "sources": [ | |
| 431 | + "favikon_qc", | |
| 432 | + "firecrawl_enrich" | |
| 433 | + ], | |
| 434 | + "total_followers": 2428000, | |
| 435 | + "platform_count": 2, | |
| 436 | + "tier": "méga (1M+)" | |
| 437 | + }, | |
| 438 | + { | |
| 439 | + "name": "Jahan Tea", | |
| 440 | + "handles": { | |
| 441 | + "instagram": null, | |
| 442 | + "tiktok": "iamjahantea", | |
| 443 | + "youtube": null, | |
| 444 | + "facebook": null, | |
| 445 | + "x": null, | |
| 446 | + "twitch": null | |
| 447 | + }, | |
| 448 | + "metrics": { | |
| 449 | + "instagram_followers": 282000, | |
| 450 | + "tiktok_followers": 1300000, | |
| 451 | + "youtube_subscribers": 721000, | |
| 452 | + "facebook_followers": null, | |
| 453 | + "x_followers": null, | |
| 454 | + "twitch_followers": null | |
| 455 | + }, | |
| 456 | + "category": "mode", | |
| 457 | + "location": "Montréal", | |
| 458 | + "notes": "Mode, lifestyle et humour; vibe « French girl from Montréal ». #9 Favikon Québec (1,8M cumulés).", | |
| 459 | + "sources": [ | |
| 460 | + "metricool", | |
| 461 | + "favikon_qc" | |
| 462 | + ], | |
| 463 | + "total_followers": 2303000, | |
| 464 | + "platform_count": 3, | |
| 465 | + "tier": "méga (1M+)" | |
| 466 | + }, | |
| 467 | + { | |
| 468 | + "name": "Nabil Lahrech", | |
| 469 | + "handles": { | |
| 470 | + "instagram": "aiequi", | |
| 471 | + "tiktok": "nabil_lahrech", | |
| 472 | + "youtube": "aiekillu", | |
| 473 | + "facebook": null, | |
| 474 | + "x": null, | |
| 475 | + "twitch": null | |
| 476 | + }, | |
| 477 | + "metrics": { | |
| 478 | + "instagram_followers": 226000, | |
| 479 | + "tiktok_followers": 71600, | |
| 480 | + "youtube_subscribers": 1700000, | |
| 481 | + "facebook_followers": null, | |
| 482 | + "x_followers": 256600, | |
| 483 | + "twitch_followers": null | |
| 484 | + }, | |
| 485 | + "category": "gaming", | |
| 486 | + "location": "Montréal", | |
| 487 | + "notes": "youtubeur très populaire, coanimateur du podcast Sous Influence | Jeux vidéo et vie de papa. Chiffres 2022. Aussi #4 (gaming) au top 100 Titan 2022. | Figure du gaming francophone depuis 10+ ans; vidéos immersives de jeux de survie (7 Days to Die, The Forest, Contraband Police). | enrichi via recherche web 2026-08; alias Aiekillu", | |
| 488 | + "sources": [ | |
| 489 | + "billie", | |
| 490 | + "billie_youtube", | |
| 491 | + "titan_youtube_2022", | |
| 492 | + "metricool", | |
| 493 | + "firecrawl_enrich" | |
| 494 | + ], | |
| 495 | + "total_followers": 2254200, | |
| 496 | + "platform_count": 4, | |
| 497 | + "tier": "méga (1M+)" | |
| 498 | + }, | |
| 499 | + { | |
| 500 | + "name": "Elisabeth Rioux", | |
| 501 | + "handles": { | |
| 502 | + "instagram": "elisabeth.rioux", | |
| 503 | + "tiktok": "elisabeth.rioux", | |
| 504 | + "youtube": "elisabeth rioux", | |
| 505 | + "facebook": "elisabeth.rioux", | |
| 506 | + "x": null, | |
| 507 | + "twitch": null | |
| 508 | + }, | |
| 509 | + "metrics": { | |
| 510 | + "instagram_followers": 1800000, | |
| 511 | + "tiktok_followers": 141200, | |
| 512 | + "youtube_subscribers": 274000, | |
| 513 | + "facebook_followers": 25100, | |
| 514 | + "x_followers": null, | |
| 515 | + "twitch_followers": null | |
| 516 | + }, | |
| 517 | + "category": "entrepreneur", | |
| 518 | + "location": "Blainville, Québec", | |
| 519 | + "notes": "femme d'affaires, fondatrice de Hoaka Swimwear, Hoaka Apparel et Tasti | Vlogs sur sa vie, ses projets d'affaires et ses voyages. Chiffres 2022. | Fondatrice de Hoaka Swimwear à 18 ans; téléréalité Élisabeth Rioux: Unfiltered sur Prime Video (2024). #1 du top mainstream canadult (revenus estimés 1M$–5M$+). | Entrepreneure (Hoaka Swimwear); 1,8M abonnés IG selon l'article (embed récent: 1M); émission Influenceurs à VRAK", | |
| 520 | + "sources": [ | |
| 521 | + "billie", | |
| 522 | + "billie_youtube", | |
| 523 | + "frank", | |
| 524 | + "metricool", | |
| 525 | + "canadult", | |
| 526 | + "noovomoi_voyage" | |
| 527 | + ], | |
| 528 | + "total_followers": 2240300, | |
| 529 | + "platform_count": 4, | |
| 530 | + "tier": "méga (1M+)" | |
| 531 | + }, | |
| 532 | + { | |
| 533 | + "name": "Eugenie Bouchard", | |
| 534 | + "handles": { | |
| 535 | + "instagram": "geniebouchard", | |
| 536 | + "tiktok": "geniebouchard", | |
| 537 | + "youtube": null, | |
| 538 | + "facebook": null, | |
| 539 | + "x": null, | |
| 540 | + "twitch": null | |
| 541 | + }, | |
| 542 | + "metrics": { | |
| 543 | + "instagram_followers": 2200000, | |
| 544 | + "tiktok_followers": 24300, | |
| 545 | + "youtube_subscribers": null, | |
| 546 | + "facebook_followers": null, | |
| 547 | + "x_followers": null, | |
| 548 | + "twitch_followers": null | |
| 549 | + }, | |
| 550 | + "category": "sport", | |
| 551 | + "location": "Montréal, QC", | |
| 552 | + "notes": "Joueuse de tennis; mention comparative dans l'article (2.1M sur Instagram). Chiffres ~2021-2022. | enrichi via recherche web 2026-08", | |
| 553 | + "sources": [ | |
| 554 | + "alexturcotte_tiktok", | |
| 555 | + "firecrawl_enrich" | |
| 556 | + ], | |
| 557 | + "total_followers": 2224300, | |
| 558 | + "platform_count": 2, | |
| 559 | + "tier": "méga (1M+)" | |
| 560 | + }, | |
| 561 | + { | |
| 562 | + "name": "Mahdi Ba", | |
| 563 | + "handles": { | |
| 564 | + "instagram": "mahdiba", | |
| 565 | + "tiktok": "mahdibatv", | |
| 566 | + "youtube": "mahdiba", | |
| 567 | + "facebook": null, | |
| 568 | + "x": null, | |
| 569 | + "twitch": null | |
| 570 | + }, | |
| 571 | + "metrics": { | |
| 572 | + "instagram_followers": 564000, | |
| 573 | + "tiktok_followers": null, | |
| 574 | + "youtube_subscribers": 1640000, | |
| 575 | + "facebook_followers": null, | |
| 576 | + "x_followers": null, | |
| 577 | + "twitch_followers": null | |
| 578 | + }, | |
| 579 | + "category": "humour", | |
| 580 | + "location": "Québec", | |
| 581 | + "notes": "youtubeur populaire (orthographié « Madhi Ba » dans l'article Billie) | Ex-gamer devenu créateur humoristique, aussi production musicale. Chiffres 2022. | enrichi via recherche web 2026-08", | |
| 582 | + "sources": [ | |
| 583 | + "billie", | |
| 584 | + "billie_youtube", | |
| 585 | + "firecrawl_enrich" | |
| 586 | + ], | |
| 587 | + "total_followers": 2204000, | |
| 588 | + "platform_count": 3, | |
| 589 | + "tier": "méga (1M+)" | |
| 590 | + }, | |
| 591 | + { | |
| 592 | + "name": "Domino Santantonio", | |
| 593 | + "handles": { | |
| 594 | + "instagram": "dominosantantonio", | |
| 595 | + "tiktok": "dominosantantonio", | |
| 596 | + "youtube": "dominosantantonio", | |
| 597 | + "facebook": null, | |
| 598 | + "x": null, | |
| 599 | + "twitch": null | |
| 600 | + }, | |
| 601 | + "metrics": { | |
| 602 | + "instagram_followers": 526000, | |
| 603 | + "tiktok_followers": 1400000, | |
| 604 | + "youtube_subscribers": 264000, | |
| 605 | + "facebook_followers": null, | |
| 606 | + "x_followers": null, | |
| 607 | + "twitch_followers": null | |
| 608 | + }, | |
| 609 | + "category": "musique", | |
| 610 | + "location": "Montréal, QC", | |
| 611 | + "notes": "Batteure virtuose, contenu musical. Chiffres ~2021-2022. | enrichi via recherche web 2026-08", | |
| 612 | + "sources": [ | |
| 613 | + "alexturcotte_tiktok", | |
| 614 | + "firecrawl_enrich" | |
| 615 | + ], | |
| 616 | + "total_followers": 2190000, | |
| 617 | + "platform_count": 3, | |
| 618 | + "tier": "méga (1M+)" | |
| 619 | + }, | |
| 620 | + { | |
| 621 | + "name": "Emy Lalune", | |
| 622 | + "handles": { | |
| 623 | + "instagram": "emylalune", | |
| 624 | + "tiktok": "emylalune", | |
| 625 | + "youtube": "emy lalune", | |
| 626 | + "facebook": null, | |
| 627 | + "x": null, | |
| 628 | + "twitch": null | |
| 629 | + }, | |
| 630 | + "metrics": { | |
| 631 | + "instagram_followers": 208000, | |
| 632 | + "tiktok_followers": 1700000, | |
| 633 | + "youtube_subscribers": 237000, | |
| 634 | + "facebook_followers": null, | |
| 635 | + "x_followers": null, | |
| 636 | + "twitch_followers": null | |
| 637 | + }, | |
| 638 | + "category": "humour", | |
| 639 | + "location": "Québec", | |
| 640 | + "notes": "Sketchs et humour décalé; projets en littérature jeunesse. TikTok chiffres ~2021-2022; classée top YouTubeurs QC 2026 par ONEWAY. | Gagnante Gala InfluenceCréation 2025 — TikTokeuse de l'année. Hauls, friperie et mode durable; 1,7M abonnés cumulés selon Favikon (#13). | enrichi via recherche web 2026-08", | |
| 641 | + "sources": [ | |
| 642 | + "alexturcotte_tiktok", | |
| 643 | + "oneway_youtube", | |
| 644 | + "billie_youtube", | |
| 645 | + "billie_gala", | |
| 646 | + "grenier_gala", | |
| 647 | + "favikon_qc", | |
| 648 | + "firecrawl_enrich" | |
| 649 | + ], | |
| 650 | + "total_followers": 2145000, | |
| 651 | + "platform_count": 3, | |
| 652 | + "tier": "méga (1M+)" | |
| 653 | + }, | |
| 654 | + { | |
| 655 | + "name": "Carl is cooking", | |
| 656 | + "handles": { | |
| 657 | + "instagram": "carlarsenault", | |
| 658 | + "tiktok": "carl.is.cooking", | |
| 659 | + "youtube": "carl is cooking", | |
| 660 | + "facebook": null, | |
| 661 | + "x": null, | |
| 662 | + "twitch": null | |
| 663 | + }, | |
| 664 | + "metrics": { | |
| 665 | + "instagram_followers": 177000, | |
| 666 | + "tiktok_followers": 427300, | |
| 667 | + "youtube_subscribers": 1400000, | |
| 668 | + "facebook_followers": null, | |
| 669 | + "x_followers": null, | |
| 670 | + "twitch_followers": null | |
| 671 | + }, | |
| 672 | + "category": "food", | |
| 673 | + "location": "Québec", | |
| 674 | + "notes": "Recettes originales publiées très régulièrement. Chiffres 2022. | Youtubeur pâtisserie et food hacks; finaliste du Meilleur Pâtissier (France); chiffres de 2019 | enrichi via recherche web 2026-08", | |
| 675 | + "sources": [ | |
| 676 | + "billie_youtube", | |
| 677 | + "clark_food", | |
| 678 | + "firecrawl_enrich" | |
| 679 | + ], | |
| 680 | + "total_followers": 2004300, | |
| 681 | + "platform_count": 3, | |
| 682 | + "tier": "méga (1M+)" | |
| 683 | + }, | |
| 684 | + { | |
| 685 | + "name": "Audrey-Anne J", | |
| 686 | + "handles": { | |
| 687 | + "instagram": "audreyannej", | |
| 688 | + "tiktok": "audreyanne.j", | |
| 689 | + "youtube": null, | |
| 690 | + "facebook": null, | |
| 691 | + "x": null, | |
| 692 | + "twitch": null | |
| 693 | + }, | |
| 694 | + "metrics": { | |
| 695 | + "instagram_followers": 1178875, | |
| 696 | + "tiktok_followers": 768500, | |
| 697 | + "youtube_subscribers": null, | |
| 698 | + "facebook_followers": null, | |
| 699 | + "x_followers": null, | |
| 700 | + "twitch_followers": null | |
| 701 | + }, | |
| 702 | + "category": "beauté", | |
| 703 | + "location": "Île-du-Prince-Édouard (québécoise)", | |
| 704 | + "notes": "idées de coiffures (hairstyles); Québécoise vivant à l'Î.-P.-É. | enrichi via recherche web 2026-08", | |
| 705 | + "sources": [ | |
| 706 | + "billie", | |
| 707 | + "firecrawl_enrich" | |
| 708 | + ], | |
| 709 | + "total_followers": 1947375, | |
| 710 | + "platform_count": 2, | |
| 711 | + "tier": "méga (1M+)" | |
| 712 | + }, | |
| 713 | + { | |
| 714 | + "name": "Claudie Mercier", | |
| 715 | + "handles": { | |
| 716 | + "instagram": "claudiemercier_", | |
| 717 | + "tiktok": "claudiemercier", | |
| 718 | + "youtube": "claudiemercier", | |
| 719 | + "facebook": null, | |
| 720 | + "x": null, | |
| 721 | + "twitch": null | |
| 722 | + }, | |
| 723 | + "metrics": { | |
| 724 | + "instagram_followers": 384000, | |
| 725 | + "tiktok_followers": 1300000, | |
| 726 | + "youtube_subscribers": 141000, | |
| 727 | + "facebook_followers": 9500, | |
| 728 | + "x_followers": null, | |
| 729 | + "twitch_followers": null | |
| 730 | + }, | |
| 731 | + "category": "voyage", | |
| 732 | + "location": "Montréal (originaire de la Beauce)", | |
| 733 | + "notes": "ex-OD Afrique du Sud, contenu voyage inspirant; 315k dans le palmarès Billie, 384K sur l'embed plus récent; top 5 L'Hebdo 2026 | Ex-candidate d'Occupation Double devenue créatrice humoristique proche des jeunes. Chiffres TikTok ~2021-2022. Aussi mentionnée sur Billie (nouveau projet 2022). | Révélée à Occupation Double Afrique du Sud (2019); humour, couple et santé mentale; livre Pas de secret entre nous (2024). #14 Favikon Québec (1,7M cumulés).", | |
| 734 | + "sources": [ | |
| 735 | + "billie", | |
| 736 | + "lhebdo", | |
| 737 | + "alexturcotte_tiktok", | |
| 738 | + "billie_youtube", | |
| 739 | + "frank", | |
| 740 | + "favikon_qc" | |
| 741 | + ], | |
| 742 | + "total_followers": 1834500, | |
| 743 | + "platform_count": 4, | |
| 744 | + "tier": "méga (1M+)" | |
| 745 | + }, | |
| 746 | + { | |
| 747 | + "name": "Gurky", | |
| 748 | + "handles": { | |
| 749 | + "instagram": "legurky", | |
| 750 | + "tiktok": "gurkytiktok", | |
| 751 | + "youtube": "legurky", | |
| 752 | + "facebook": null, | |
| 753 | + "x": null, | |
| 754 | + "twitch": null | |
| 755 | + }, | |
| 756 | + "metrics": { | |
| 757 | + "instagram_followers": 183000, | |
| 758 | + "tiktok_followers": 195900, | |
| 759 | + "youtube_subscribers": 1450000, | |
| 760 | + "facebook_followers": null, | |
| 761 | + "x_followers": null, | |
| 762 | + "twitch_followers": null | |
| 763 | + }, | |
| 764 | + "category": "food", | |
| 765 | + "location": "Québec", | |
| 766 | + "notes": "Défis culinaires et dégustations improbables. Top YouTubeurs QC 2026 (ONEWAY); #8 (vlog) au top 100 Titan 2022. | enrichi via recherche web 2026-08", | |
| 767 | + "sources": [ | |
| 768 | + "oneway_youtube", | |
| 769 | + "titan_youtube_2022", | |
| 770 | + "firecrawl_enrich" | |
| 771 | + ], | |
| 772 | + "total_followers": 1828900, | |
| 773 | + "platform_count": 3, | |
| 774 | + "tier": "méga (1M+)" | |
| 775 | + }, | |
| 776 | + { | |
| 777 | + "name": "Claudia Tihan", | |
| 778 | + "handles": { | |
| 779 | + "instagram": "claudiatihan", | |
| 780 | + "tiktok": null, | |
| 781 | + "youtube": null, | |
| 782 | + "facebook": null, | |
| 783 | + "x": null, | |
| 784 | + "twitch": null | |
| 785 | + }, | |
| 786 | + "metrics": { | |
| 787 | + "instagram_followers": 1400000, | |
| 788 | + "tiktok_followers": 365400, | |
| 789 | + "youtube_subscribers": null, | |
| 790 | + "facebook_followers": null, | |
| 791 | + "x_followers": null, | |
| 792 | + "twitch_followers": null | |
| 793 | + }, | |
| 794 | + "category": "mode", | |
| 795 | + "location": "Montréal", | |
| 796 | + "notes": "mannequin, voyages autour du monde | enrichi via recherche web 2026-08", | |
| 797 | + "sources": [ | |
| 798 | + "billie", | |
| 799 | + "firecrawl_enrich" | |
| 800 | + ], | |
| 801 | + "total_followers": 1765400, | |
| 802 | + "platform_count": 2, | |
| 803 | + "tier": "méga (1M+)" | |
| 804 | + }, | |
| 805 | + { | |
| 806 | + "name": "Franceska Fournier", | |
| 807 | + "handles": { | |
| 808 | + "instagram": "franceskafournier", | |
| 809 | + "tiktok": "franceskafournier", | |
| 810 | + "youtube": null, | |
| 811 | + "facebook": null, | |
| 812 | + "x": null, | |
| 813 | + "twitch": null | |
| 814 | + }, | |
| 815 | + "metrics": { | |
| 816 | + "instagram_followers": 1200000, | |
| 817 | + "tiktok_followers": 547000, | |
| 818 | + "youtube_subscribers": null, | |
| 819 | + "facebook_followers": null, | |
| 820 | + "x_followers": null, | |
| 821 | + "twitch_followers": null | |
| 822 | + }, | |
| 823 | + "category": "mode", | |
| 824 | + "location": "Québec", | |
| 825 | + "notes": "mannequin et influenceuse voyage | enrichi via recherche web 2026-08", | |
| 826 | + "sources": [ | |
| 827 | + "billie", | |
| 828 | + "firecrawl_enrich" | |
| 829 | + ], | |
| 830 | + "total_followers": 1747000, | |
| 831 | + "platform_count": 2, | |
| 832 | + "tier": "méga (1M+)" | |
| 833 | + }, | |
| 834 | + { | |
| 835 | + "name": "Alex Centomo", | |
| 836 | + "handles": { | |
| 837 | + "instagram": "alexcentomo", | |
| 838 | + "tiktok": "alex_centomo", | |
| 839 | + "youtube": "alexcentomo", | |
| 840 | + "facebook": null, | |
| 841 | + "x": null, | |
| 842 | + "twitch": null | |
| 843 | + }, | |
| 844 | + "metrics": { | |
| 845 | + "instagram_followers": 970000, | |
| 846 | + "tiktok_followers": 80300, | |
| 847 | + "youtube_subscribers": 578000, | |
| 848 | + "facebook_followers": null, | |
| 849 | + "x_followers": null, | |
| 850 | + "twitch_followers": null | |
| 851 | + }, | |
| 852 | + "category": "mode", | |
| 853 | + "location": "Québec", | |
| 854 | + "notes": "contenu mode, beauté et body positive; jeune maman | enrichi via recherche web 2026-08", | |
| 855 | + "sources": [ | |
| 856 | + "billie", | |
| 857 | + "firecrawl_enrich" | |
| 858 | + ], | |
| 859 | + "total_followers": 1628300, | |
| 860 | + "platform_count": 3, | |
| 861 | + "tier": "méga (1M+)" | |
| 862 | + }, | |
| 863 | + { | |
| 864 | + "name": "Frank Leblanc (No Fancy Shit)", | |
| 865 | + "handles": { | |
| 866 | + "instagram": "nofancyshit", | |
| 867 | + "tiktok": "nofancyshit", | |
| 868 | + "youtube": null, | |
| 869 | + "facebook": null, | |
| 870 | + "x": null, | |
| 871 | + "twitch": null | |
| 872 | + }, | |
| 873 | + "metrics": { | |
| 874 | + "instagram_followers": null, | |
| 875 | + "tiktok_followers": 1500000, | |
| 876 | + "youtube_subscribers": null, | |
| 877 | + "facebook_followers": null, | |
| 878 | + "x_followers": null, | |
| 879 | + "twitch_followers": null | |
| 880 | + }, | |
| 881 | + "category": "food", | |
| 882 | + "location": "Québec", | |
| 883 | + "notes": "Cuisine « no fancy » humoristique, sans montrer son visage ni sa voix. Chiffres ~2021-2022. | enrichi via recherche web 2026-08", | |
| 884 | + "sources": [ | |
| 885 | + "alexturcotte_tiktok", | |
| 886 | + "firecrawl_enrich" | |
| 887 | + ], | |
| 888 | + "total_followers": 1500000, | |
| 889 | + "platform_count": 2, | |
| 890 | + "tier": "méga (1M+)" | |
| 891 | + }, | |
| 892 | + { | |
| 893 | + "name": "Victoria Charlton", | |
| 894 | + "handles": { | |
| 895 | + "instagram": "victoria.charlton", | |
| 896 | + "tiktok": "victoriacharltonn", | |
| 897 | + "youtube": "victoria charlton", | |
| 898 | + "facebook": "victoriacharltonofficiel", | |
| 899 | + "x": null, | |
| 900 | + "twitch": null | |
| 901 | + }, | |
| 902 | + "metrics": { | |
| 903 | + "instagram_followers": 174000, | |
| 904 | + "tiktok_followers": 47700, | |
| 905 | + "youtube_subscribers": 755000, | |
| 906 | + "facebook_followers": 374000, | |
| 907 | + "x_followers": null, | |
| 908 | + "twitch_followers": null | |
| 909 | + }, | |
| 910 | + "category": "autre", | |
| 911 | + "location": "Québec", | |
| 912 | + "notes": "Pionnière du true crime au Québec (enquêtes, paranormal). Abonnés YouTube : chiffres 2022. Aussi #9 au top 100 Titan 2022 et top YouTubeurs 2026 (ONEWAY). | Référence du true crime francophone sur YouTube depuis 2014; autrice (Gardez l'œil ouvert), balados Post-Mortem et Trip de bouffe.", | |
| 913 | + "sources": [ | |
| 914 | + "billie_youtube", | |
| 915 | + "titan_youtube_2022", | |
| 916 | + "oneway_youtube", | |
| 917 | + "frank", | |
| 918 | + "metricool", | |
| 919 | + "canadult" | |
| 920 | + ], | |
| 921 | + "total_followers": 1350700, | |
| 922 | + "platform_count": 4, | |
| 923 | + "tier": "méga (1M+)" | |
| 924 | + }, | |
| 925 | + { | |
| 926 | + "name": "Mike Chabot", | |
| 927 | + "handles": { | |
| 928 | + "instagram": "mikechabotfitness", | |
| 929 | + "tiktok": "rewiredbymike", | |
| 930 | + "youtube": "mikechabot", | |
| 931 | + "facebook": null, | |
| 932 | + "x": null, | |
| 933 | + "twitch": null | |
| 934 | + }, | |
| 935 | + "metrics": { | |
| 936 | + "instagram_followers": 1300000, | |
| 937 | + "tiktok_followers": null, | |
| 938 | + "youtube_subscribers": 14600, | |
| 939 | + "facebook_followers": null, | |
| 940 | + "x_followers": null, | |
| 941 | + "twitch_followers": null | |
| 942 | + }, | |
| 943 | + "category": "fitness", | |
| 944 | + "location": "Québec", | |
| 945 | + "notes": "ex-candidat de la télé-réalité XOXO, contenu fitness et inspirant | enrichi via recherche web 2026-08", | |
| 946 | + "sources": [ | |
| 947 | + "billie", | |
| 948 | + "firecrawl_enrich" | |
| 949 | + ], | |
| 950 | + "total_followers": 1314600, | |
| 951 | + "platform_count": 3, | |
| 952 | + "tier": "méga (1M+)" | |
| 953 | + }, | |
| 954 | + { | |
| 955 | + "name": "Marianne Plaisance (2e peau)", | |
| 956 | + "handles": { | |
| 957 | + "instagram": "marianne.plaisance", | |
| 958 | + "tiktok": "marianneplaisance", | |
| 959 | + "youtube": "2e peau", | |
| 960 | + "facebook": "2epeau", | |
| 961 | + "x": null, | |
| 962 | + "twitch": null | |
| 963 | + }, | |
| 964 | + "metrics": { | |
| 965 | + "instagram_followers": 516000, | |
| 966 | + "tiktok_followers": 514100, | |
| 967 | + "youtube_subscribers": 240000, | |
| 968 | + "facebook_followers": 38940, | |
| 969 | + "x_followers": null, | |
| 970 | + "twitch_followers": null | |
| 971 | + }, | |
| 972 | + "category": "lifestyle", | |
| 973 | + "location": "Québec", | |
| 974 | + "notes": "Contenu créatif et original; le duo 2e peau s'est séparé mais Marianne poursuit la chaîne. Chiffres 2022. Aussi #17 (vlog) au top 100 Titan 2022. | enrichi via recherche web 2026-08; facebook = J'aime de page", | |
| 975 | + "sources": [ | |
| 976 | + "billie_youtube", | |
| 977 | + "titan_youtube_2022", | |
| 978 | + "firecrawl_enrich" | |
| 979 | + ], | |
| 980 | + "total_followers": 1309040, | |
| 981 | + "platform_count": 4, | |
| 982 | + "tier": "méga (1M+)" | |
| 983 | + }, | |
| 984 | + { | |
| 985 | + "name": "Alicia Moffet", | |
| 986 | + "handles": { | |
| 987 | + "instagram": "aliciamoffet", | |
| 988 | + "tiktok": "aliciamoffet", | |
| 989 | + "youtube": "alicia moffet", | |
| 990 | + "facebook": "aliciamoffetofficial", | |
| 991 | + "x": null, | |
| 992 | + "twitch": null | |
| 993 | + }, | |
| 994 | + "metrics": { | |
| 995 | + "instagram_followers": 475000, | |
| 996 | + "tiktok_followers": 498300, | |
| 997 | + "youtube_subscribers": 221000, | |
| 998 | + "facebook_followers": 90000, | |
| 999 | + "x_followers": null, | |
| 1000 | + "twitch_followers": null | |
| 1001 | + }, | |
| 1002 | + "category": "musique", | |
| 1003 | + "location": "Québec", | |
| 1004 | + "notes": "chanteuse et maman; citée dans le top 5 québécois 2026 de L'Hebdo | Chanteuse; vidéoclips sur YouTube. Chiffres 2022. | Chanteuse pop (The Next Star 2013, La Voix 2015), marque Silly Billy, coanime Occupation Double depuis 2023. Revenus estimés 500k$–2M$ (canadult).", | |
| 1005 | + "sources": [ | |
| 1006 | + "billie", | |
| 1007 | + "lhebdo", | |
| 1008 | + "billie_youtube", | |
| 1009 | + "frank", | |
| 1010 | + "canadult" | |
| 1011 | + ], | |
| 1012 | + "total_followers": 1284300, | |
| 1013 | + "platform_count": 4, | |
| 1014 | + "tier": "méga (1M+)" | |
| 1015 | + }, | |
| 1016 | + { | |
| 1017 | + "name": "Lysandre Nadeau", | |
| 1018 | + "handles": { | |
| 1019 | + "instagram": "lysandrenadeau", | |
| 1020 | + "tiktok": "lysandrenadeau", | |
| 1021 | + "youtube": "lysandrenadeau", | |
| 1022 | + "facebook": "nadeaulysandre", | |
| 1023 | + "x": null, | |
| 1024 | + "twitch": null | |
| 1025 | + }, | |
| 1026 | + "metrics": { | |
| 1027 | + "instagram_followers": 453000, | |
| 1028 | + "tiktok_followers": 335400, | |
| 1029 | + "youtube_subscribers": 420000, | |
| 1030 | + "facebook_followers": 48000, | |
| 1031 | + "x_followers": null, | |
| 1032 | + "twitch_followers": null | |
| 1033 | + }, | |
| 1034 | + "category": "lifestyle", | |
| 1035 | + "location": "Québec", | |
| 1036 | + "notes": "youtubeuse transparente sur sa vie; citée dans le top 5 québécois 2026 de L'Hebdo | Vlogs honnêtes; inactive depuis plus d'un an au moment de l'article. Chiffres 2022. | Vlogs YouTube (sexualité, santé mentale, féminisme); balados Sexe Oral et Parenthèse — Balado de l'année, Gala InfluenceCréation 2025 (avec Claude Bégin); Big Brother Célébrités 2021; copropriétaire Statera Spirits. Aussi listée par canadult en contenu hybride lifestyle/premium (OnlyFans).", | |
| 1037 | + "sources": [ | |
| 1038 | + "billie", | |
| 1039 | + "lhebdo", | |
| 1040 | + "billie_youtube", | |
| 1041 | + "frank", | |
| 1042 | + "canadult", | |
| 1043 | + "billie_gala", | |
| 1044 | + "grenier_gala" | |
| 1045 | + ], | |
| 1046 | + "total_followers": 1256400, | |
| 1047 | + "platform_count": 4, | |
| 1048 | + "tier": "méga (1M+)" | |
| 1049 | + }, | |
| 1050 | + { | |
| 1051 | + "name": "Aly Brassard", | |
| 1052 | + "handles": { | |
| 1053 | + "instagram": "alybrassard", | |
| 1054 | + "tiktok": "alybrassard", | |
| 1055 | + "youtube": "aly brassard", | |
| 1056 | + "facebook": null, | |
| 1057 | + "x": null, | |
| 1058 | + "twitch": null | |
| 1059 | + }, | |
| 1060 | + "metrics": { | |
| 1061 | + "instagram_followers": 253000, | |
| 1062 | + "tiktok_followers": 745100, | |
| 1063 | + "youtube_subscribers": 100000, | |
| 1064 | + "facebook_followers": null, | |
| 1065 | + "x_followers": null, | |
| 1066 | + "twitch_followers": null | |
| 1067 | + }, | |
| 1068 | + "category": "humour", | |
| 1069 | + "location": "Québec", | |
| 1070 | + "notes": "Sketchs, vlogs, magie et humour du quotidien. Top YouTubeurs QC 2026 (ONEWAY). | Gagnante Gala InfluenceCréation 2025 — Créatrice de l'année et Instagrameuse de l'année (prix remis par le ministre Mathieu Lacombe). Sketchs comiques, magie et vlogs sur YouTube/TikTok; 914,9K abonnés cumulés selon Favikon (#18). | Active sur YouTube, TikTok et Instagram; sketches comiques, moments de vie et tours de magie; top Twitch QC 2026 | enrichi via recherche web 2026-08", | |
| 1071 | + "sources": [ | |
| 1072 | + "oneway_youtube", | |
| 1073 | + "billie_gala", | |
| 1074 | + "grenier_gala", | |
| 1075 | + "favikon_qc", | |
| 1076 | + "leregard_twitch", | |
| 1077 | + "firecrawl_enrich" | |
| 1078 | + ], | |
| 1079 | + "total_followers": 1098100, | |
| 1080 | + "platform_count": 3, | |
| 1081 | + "tier": "méga (1M+)" | |
| 1082 | + }, | |
| 1083 | + { | |
| 1084 | + "name": "Catherine Francoeur (girlyaddict)", | |
| 1085 | + "handles": { | |
| 1086 | + "instagram": "girlyaddict", | |
| 1087 | + "tiktok": null, | |
| 1088 | + "youtube": "girlyaddict", | |
| 1089 | + "facebook": null, | |
| 1090 | + "x": null, | |
| 1091 | + "twitch": null | |
| 1092 | + }, | |
| 1093 | + "metrics": { | |
| 1094 | + "instagram_followers": 151000, | |
| 1095 | + "tiktok_followers": null, | |
| 1096 | + "youtube_subscribers": 829000, | |
| 1097 | + "facebook_followers": null, | |
| 1098 | + "x_followers": null, | |
| 1099 | + "twitch_followers": null | |
| 1100 | + }, | |
| 1101 | + "category": "lifestyle", | |
| 1102 | + "location": "Québec", | |
| 1103 | + "notes": "DIY, défis et conseils. Chiffres 2022. Aussi #12 (beauté) au top 100 Titan 2022. | enrichi via recherche web 2026-08", | |
| 1104 | + "sources": [ | |
| 1105 | + "billie_youtube", | |
| 1106 | + "titan_youtube_2022", | |
| 1107 | + "firecrawl_enrich" | |
| 1108 | + ], | |
| 1109 | + "total_followers": 980000, | |
| 1110 | + "platform_count": 2, | |
| 1111 | + "tier": "macro (100K+)" | |
| 1112 | + }, | |
| 1113 | + { | |
| 1114 | + "name": "Olivier Primeau", | |
| 1115 | + "handles": { | |
| 1116 | + "instagram": "olivierprimeaubc", | |
| 1117 | + "tiktok": "oliprimeau", | |
| 1118 | + "youtube": "olivierprimeau", | |
| 1119 | + "facebook": null, | |
| 1120 | + "x": null, | |
| 1121 | + "twitch": null | |
| 1122 | + }, | |
| 1123 | + "metrics": { | |
| 1124 | + "instagram_followers": 654000, | |
| 1125 | + "tiktok_followers": 203000, | |
| 1126 | + "youtube_subscribers": 8540, | |
| 1127 | + "facebook_followers": 94000, | |
| 1128 | + "x_followers": null, | |
| 1129 | + "twitch_followers": null | |
| 1130 | + }, | |
| 1131 | + "category": "entrepreneur", | |
| 1132 | + "location": "Pointe-Calumet", | |
| 1133 | + "notes": "entrepreneur (Beach Club); ex-image de party boy devenue marque lifestyle complète; top 5 L'Hebdo 2026 | Entrepreneur derrière le Beachclub, festivals Métro Métro et Escapade, marque Beach Day Every Day, restos Slice Gang et Jack le Coq; critiques de poutines.", | |
| 1134 | + "sources": [ | |
| 1135 | + "lhebdo", | |
| 1136 | + "frank", | |
| 1137 | + "canadult" | |
| 1138 | + ], | |
| 1139 | + "total_followers": 959540, | |
| 1140 | + "platform_count": 4, | |
| 1141 | + "tier": "macro (100K+)" | |
| 1142 | + }, | |
| 1143 | + { | |
| 1144 | + "name": "Emma Verde", | |
| 1145 | + "handles": { | |
| 1146 | + "instagram": "emmaverdeyt", | |
| 1147 | + "tiktok": "emmaaaaverde", | |
| 1148 | + "youtube": "emmaverde", | |
| 1149 | + "facebook": null, | |
| 1150 | + "x": null, | |
| 1151 | + "twitch": null | |
| 1152 | + }, | |
| 1153 | + "metrics": { | |
| 1154 | + "instagram_followers": 285000, | |
| 1155 | + "tiktok_followers": null, | |
| 1156 | + "youtube_subscribers": 666000, | |
| 1157 | + "facebook_followers": null, | |
| 1158 | + "x_followers": null, | |
| 1159 | + "twitch_followers": null | |
| 1160 | + }, | |
| 1161 | + "category": "lifestyle", | |
| 1162 | + "location": "Québec", | |
| 1163 | + "notes": "youtubeuse, moins présente sur les réseaux qu'avant | Déco, voyages, mode et vie en appartement. Chiffres 2022. | enrichi via recherche web 2026-08", | |
| 1164 | + "sources": [ | |
| 1165 | + "billie", | |
| 1166 | + "billie_youtube", | |
| 1167 | + "firecrawl_enrich" | |
| 1168 | + ], | |
| 1169 | + "total_followers": 951000, | |
| 1170 | + "platform_count": 3, | |
| 1171 | + "tier": "macro (100K+)" | |
| 1172 | + }, | |
| 1173 | + { | |
| 1174 | + "name": "Amine Laabi", | |
| 1175 | + "handles": { | |
| 1176 | + "instagram": "just.another.cook", | |
| 1177 | + "tiktok": null, | |
| 1178 | + "youtube": null, | |
| 1179 | + "facebook": null, | |
| 1180 | + "x": null, | |
| 1181 | + "twitch": null | |
| 1182 | + }, | |
| 1183 | + "metrics": { | |
| 1184 | + "instagram_followers": 930000, | |
| 1185 | + "tiktok_followers": null, | |
| 1186 | + "youtube_subscribers": null, | |
| 1187 | + "facebook_followers": null, | |
| 1188 | + "x_followers": null, | |
| 1189 | + "twitch_followers": null | |
| 1190 | + }, | |
| 1191 | + "category": "food", | |
| 1192 | + "location": "Québec", | |
| 1193 | + "notes": "Cuisinier créateur (Macro); @loumimtl", | |
| 1194 | + "sources": [ | |
| 1195 | + "feedspot_food" | |
| 1196 | + ], | |
| 1197 | + "total_followers": 930000, | |
| 1198 | + "platform_count": 1, | |
| 1199 | + "tier": "macro (100K+)" | |
| 1200 | + }, | |
| 1201 | + { | |
| 1202 | + "name": "Cynthia Dulude", | |
| 1203 | + "handles": { | |
| 1204 | + "instagram": "cynthiadulude", | |
| 1205 | + "tiktok": "cynthia.dulude", | |
| 1206 | + "youtube": "cynthiadulude", | |
| 1207 | + "facebook": null, | |
| 1208 | + "x": null, | |
| 1209 | + "twitch": null | |
| 1210 | + }, | |
| 1211 | + "metrics": { | |
| 1212 | + "instagram_followers": 215000, | |
| 1213 | + "tiktok_followers": null, | |
| 1214 | + "youtube_subscribers": 685000, | |
| 1215 | + "facebook_followers": null, | |
| 1216 | + "x_followers": null, | |
| 1217 | + "twitch_followers": null | |
| 1218 | + }, | |
| 1219 | + "category": "beauté", | |
| 1220 | + "location": "Montréal, QC", | |
| 1221 | + "notes": "Maquilleuse : tutoriels et astuces depuis plus de 10 ans. Chiffres 2022. Aussi #13 (beauté) au top 100 Titan 2022. | Référence beauté francophone (canadult, revenus estimés 250k$–1M$); gagnante Gala InfluenceCréation 2025 — Mode & beauté. | enrichi via recherche web 2026-08", | |
| 1222 | + "sources": [ | |
| 1223 | + "billie_youtube", | |
| 1224 | + "titan_youtube_2022", | |
| 1225 | + "canadult", | |
| 1226 | + "billie_gala", | |
| 1227 | + "grenier_gala", | |
| 1228 | + "firecrawl_enrich" | |
| 1229 | + ], | |
| 1230 | + "total_followers": 900000, | |
| 1231 | + "platform_count": 3, | |
| 1232 | + "tier": "macro (100K+)" | |
| 1233 | + }, | |
| 1234 | + { | |
| 1235 | + "name": "Jess Megan", | |
| 1236 | + "handles": { | |
| 1237 | + "instagram": "jessmegan", | |
| 1238 | + "tiktok": "_jessmegan", | |
| 1239 | + "youtube": null, | |
| 1240 | + "facebook": null, | |
| 1241 | + "x": null, | |
| 1242 | + "twitch": null | |
| 1243 | + }, | |
| 1244 | + "metrics": { | |
| 1245 | + "instagram_followers": 408000, | |
| 1246 | + "tiktok_followers": 394100, | |
| 1247 | + "youtube_subscribers": null, | |
| 1248 | + "facebook_followers": null, | |
| 1249 | + "x_followers": null, | |
| 1250 | + "twitch_followers": null | |
| 1251 | + }, | |
| 1252 | + "category": "lifestyle", | |
| 1253 | + "location": "Montréal", | |
| 1254 | + "notes": "Top 20 Modash Montréal, rang 9; maman de deux enfants, voyages et vie quotidienne; engagement 2.19% | enrichi via recherche web 2026-08", | |
| 1255 | + "sources": [ | |
| 1256 | + "modash", | |
| 1257 | + "firecrawl_enrich" | |
| 1258 | + ], | |
| 1259 | + "total_followers": 802100, | |
| 1260 | + "platform_count": 2, | |
| 1261 | + "tier": "macro (100K+)" | |
| 1262 | + }, | |
| 1263 | + { | |
| 1264 | + "name": "Sami Landri", | |
| 1265 | + "handles": { | |
| 1266 | + "instagram": "samilandri", | |
| 1267 | + "tiktok": "samilandri", | |
| 1268 | + "youtube": null, | |
| 1269 | + "facebook": null, | |
| 1270 | + "x": null, | |
| 1271 | + "twitch": null | |
| 1272 | + }, | |
| 1273 | + "metrics": { | |
| 1274 | + "instagram_followers": 293700, | |
| 1275 | + "tiktok_followers": 493900, | |
| 1276 | + "youtube_subscribers": null, | |
| 1277 | + "facebook_followers": null, | |
| 1278 | + "x_followers": null, | |
| 1279 | + "twitch_followers": null | |
| 1280 | + }, | |
| 1281 | + "category": "humour", | |
| 1282 | + "location": "Montréal", | |
| 1283 | + "notes": "Top 20 Modash Montréal, rang 3; drag queen acadienne basée à Montréal (Canada's Drag Race); engagement 4.28% | enrichi via recherche web 2026-08", | |
| 1284 | + "sources": [ | |
| 1285 | + "modash", | |
| 1286 | + "firecrawl_enrich" | |
| 1287 | + ], | |
| 1288 | + "total_followers": 787600, | |
| 1289 | + "platform_count": 2, | |
| 1290 | + "tier": "macro (100K+)" | |
| 1291 | + }, | |
| 1292 | + { | |
| 1293 | + "name": "Marina Totino", | |
| 1294 | + "handles": { | |
| 1295 | + "instagram": "marinaminis", | |
| 1296 | + "tiktok": null, | |
| 1297 | + "youtube": null, | |
| 1298 | + "facebook": "marinatotinophotography", | |
| 1299 | + "x": null, | |
| 1300 | + "twitch": null | |
| 1301 | + }, | |
| 1302 | + "metrics": { | |
| 1303 | + "instagram_followers": 288900, | |
| 1304 | + "tiktok_followers": 460000, | |
| 1305 | + "youtube_subscribers": null, | |
| 1306 | + "facebook_followers": null, | |
| 1307 | + "x_followers": null, | |
| 1308 | + "twitch_followers": null | |
| 1309 | + }, | |
| 1310 | + "category": "autre", | |
| 1311 | + "location": "Montréal", | |
| 1312 | + "notes": "Top 20 Modash Montréal, rang 4; créatrice de miniatures / DIY; engagement 2.7% | enrichi via recherche web 2026-08", | |
| 1313 | + "sources": [ | |
| 1314 | + "modash", | |
| 1315 | + "firecrawl_enrich" | |
| 1316 | + ], | |
| 1317 | + "total_followers": 748900, | |
| 1318 | + "platform_count": 3, | |
| 1319 | + "tier": "macro (100K+)" | |
| 1320 | + }, | |
| 1321 | + { | |
| 1322 | + "name": "Shahin Ouest", | |
| 1323 | + "handles": { | |
| 1324 | + "instagram": "shahinouest", | |
| 1325 | + "tiktok": null, | |
| 1326 | + "youtube": "shahin ouest", | |
| 1327 | + "facebook": null, | |
| 1328 | + "x": null, | |
| 1329 | + "twitch": null | |
| 1330 | + }, | |
| 1331 | + "metrics": { | |
| 1332 | + "instagram_followers": 192000, | |
| 1333 | + "tiktok_followers": null, | |
| 1334 | + "youtube_subscribers": 550000, | |
| 1335 | + "facebook_followers": null, | |
| 1336 | + "x_followers": null, | |
| 1337 | + "twitch_followers": null | |
| 1338 | + }, | |
| 1339 | + "category": "lifestyle", | |
| 1340 | + "location": "Montréal, Québec", | |
| 1341 | + "notes": "Vidéos très produites sur Montréal et la culture francophone. Top YouTubeurs QC 2026 (ONEWAY). | Créateur humoristique sur YouTube et TikTok, contenus en France et au Canada. 601K abonnés cumulés selon Favikon (#17). | Vlogs, vidéos humoristiques et IRL; ligne de vêtements « J'comprends pas ton français » | enrichi via recherche web 2026-08", | |
| 1342 | + "sources": [ | |
| 1343 | + "oneway_youtube", | |
| 1344 | + "favikon_qc", | |
| 1345 | + "leregard_twitch", | |
| 1346 | + "firecrawl_enrich" | |
| 1347 | + ], | |
| 1348 | + "total_followers": 742000, | |
| 1349 | + "platform_count": 2, | |
| 1350 | + "tier": "macro (100K+)" | |
| 1351 | + }, | |
| 1352 | + { | |
| 1353 | + "name": "Rosalie Lessard", | |
| 1354 | + "handles": { | |
| 1355 | + "instagram": "rosalielessard_", | |
| 1356 | + "tiktok": "rosalielessard_", | |
| 1357 | + "youtube": "rosalie lessard", | |
| 1358 | + "facebook": "rosalielessardd", | |
| 1359 | + "x": null, | |
| 1360 | + "twitch": null | |
| 1361 | + }, | |
| 1362 | + "metrics": { | |
| 1363 | + "instagram_followers": 151000, | |
| 1364 | + "tiktok_followers": 59000, | |
| 1365 | + "youtube_subscribers": 490000, | |
| 1366 | + "facebook_followers": 4100, | |
| 1367 | + "x_followers": null, | |
| 1368 | + "twitch_followers": null | |
| 1369 | + }, | |
| 1370 | + "category": "food", | |
| 1371 | + "location": "Québec", | |
| 1372 | + "notes": "Chaîne axée bouffe avec humour. Chiffres YouTube 2022. Aussi #14 (alimentation) au top 100 Titan 2022. | Créatrice culinaire diplômée de l'ITHQ; livre Survivre en appart; recettes simples et lifestyle authentique. | Youtubeuse food humoristique, recettes atypiques et challenges culinaires; chiffres de 2019", | |
| 1373 | + "sources": [ | |
| 1374 | + "billie_youtube", | |
| 1375 | + "titan_youtube_2022", | |
| 1376 | + "frank", | |
| 1377 | + "clark_food" | |
| 1378 | + ], | |
| 1379 | + "total_followers": 704100, | |
| 1380 | + "platform_count": 4, | |
| 1381 | + "tier": "macro (100K+)" | |
| 1382 | + }, | |
| 1383 | + { | |
| 1384 | + "name": "Andreane Chamberland", | |
| 1385 | + "handles": { | |
| 1386 | + "instagram": "andreanec", | |
| 1387 | + "tiktok": "andreanec_", | |
| 1388 | + "youtube": null, | |
| 1389 | + "facebook": null, | |
| 1390 | + "x": null, | |
| 1391 | + "twitch": null | |
| 1392 | + }, | |
| 1393 | + "metrics": { | |
| 1394 | + "instagram_followers": 624000, | |
| 1395 | + "tiktok_followers": null, | |
| 1396 | + "youtube_subscribers": null, | |
| 1397 | + "facebook_followers": null, | |
| 1398 | + "x_followers": null, | |
| 1399 | + "twitch_followers": null | |
| 1400 | + }, | |
| 1401 | + "category": "mode", | |
| 1402 | + "location": "Québec", | |
| 1403 | + "notes": "mannequin, looks + secrets fitness et beauté | enrichi via recherche web 2026-08", | |
| 1404 | + "sources": [ | |
| 1405 | + "billie", | |
| 1406 | + "firecrawl_enrich" | |
| 1407 | + ], | |
| 1408 | + "total_followers": 624000, | |
| 1409 | + "platform_count": 2, | |
| 1410 | + "tier": "macro (100K+)" | |
| 1411 | + }, | |
| 1412 | + { | |
| 1413 | + "name": "Jérémie Latreille (Le Fit Cook)", | |
| 1414 | + "handles": { | |
| 1415 | + "instagram": "lefitcook", | |
| 1416 | + "tiktok": "lefitcook", | |
| 1417 | + "youtube": "lefitcook", | |
| 1418 | + "facebook": "lefitcook", | |
| 1419 | + "x": null, | |
| 1420 | + "twitch": null | |
| 1421 | + }, | |
| 1422 | + "metrics": { | |
| 1423 | + "instagram_followers": 166900, | |
| 1424 | + "tiktok_followers": 230000, | |
| 1425 | + "youtube_subscribers": 7800, | |
| 1426 | + "facebook_followers": 166000, | |
| 1427 | + "x_followers": null, | |
| 1428 | + "twitch_followers": null | |
| 1429 | + }, | |
| 1430 | + "category": "food", | |
| 1431 | + "location": "Montréal", | |
| 1432 | + "notes": "Créateur culinaire santé (meal prep), marque Fitcook Foodz, révélé à Un souper presque parfait; coanimateur du Gala InfluenceCréation 2025 avec Olivia Leclerc. | Fondateur FitCook, auteur x5, animateur Crave Canada FR", | |
| 1433 | + "sources": [ | |
| 1434 | + "frank", | |
| 1435 | + "grenier_gala", | |
| 1436 | + "feedspot_food" | |
| 1437 | + ], | |
| 1438 | + "total_followers": 570700, | |
| 1439 | + "platform_count": 4, | |
| 1440 | + "tier": "macro (100K+)" | |
| 1441 | + }, | |
| 1442 | + { | |
| 1443 | + "name": "Lucie Rhéaume", | |
| 1444 | + "handles": { | |
| 1445 | + "instagram": "lucierheaume", | |
| 1446 | + "tiktok": "lucie_rheaume", | |
| 1447 | + "youtube": "lucie rhéaume", | |
| 1448 | + "facebook": null, | |
| 1449 | + "x": null, | |
| 1450 | + "twitch": null | |
| 1451 | + }, | |
| 1452 | + "metrics": { | |
| 1453 | + "instagram_followers": 252000, | |
| 1454 | + "tiktok_followers": 134700, | |
| 1455 | + "youtube_subscribers": 172000, | |
| 1456 | + "facebook_followers": 3000, | |
| 1457 | + "x_followers": null, | |
| 1458 | + "twitch_followers": null | |
| 1459 | + }, | |
| 1460 | + "category": "lifestyle", | |
| 1461 | + "location": "Québec", | |
| 1462 | + "notes": "youtubeuse, cofondatrice e-Influence, Girl Crush et du podcast En Privé SVP | Contenu lifestyle diversifié avec des montages remarquables. Abonnés YouTube : chiffres 2022 (article Billie). | Double gagnante Gala InfluenceCréation 2025 — Aventure & voyage et Style de vie. Cofondatrice de Girl Crush, coanime le podcast En Privé SVP; vanlife, bien-être, santé mentale. Aussi listée par canadult (croissance lifestyle/premium, OnlyFans).", | |
| 1463 | + "sources": [ | |
| 1464 | + "billie", | |
| 1465 | + "billie_youtube", | |
| 1466 | + "frank", | |
| 1467 | + "canadult", | |
| 1468 | + "billie_gala", | |
| 1469 | + "grenier_gala" | |
| 1470 | + ], | |
| 1471 | + "total_followers": 561700, | |
| 1472 | + "platform_count": 4, | |
| 1473 | + "tier": "macro (100K+)" | |
| 1474 | + }, | |
| 1475 | + { | |
| 1476 | + "name": "Thomas Gauthier", | |
| 1477 | + "handles": { | |
| 1478 | + "instagram": "thomasgauthierinsta", | |
| 1479 | + "tiktok": null, | |
| 1480 | + "youtube": "tomlivlogs", | |
| 1481 | + "facebook": null, | |
| 1482 | + "x": null, | |
| 1483 | + "twitch": null | |
| 1484 | + }, | |
| 1485 | + "metrics": { | |
| 1486 | + "instagram_followers": 24900, | |
| 1487 | + "tiktok_followers": null, | |
| 1488 | + "youtube_subscribers": 464000, | |
| 1489 | + "facebook_followers": null, | |
| 1490 | + "x_followers": null, | |
| 1491 | + "twitch_followers": null | |
| 1492 | + }, | |
| 1493 | + "category": "humour", | |
| 1494 | + "location": "Montréal", | |
| 1495 | + "notes": "Sketches humoristiques, vidéos d'opinion et vidéoclips. Chiffres 2022. Aussi #20 (vlog) au top 100 Titan 2022. | enrichi via recherche web 2026-08", | |
| 1496 | + "sources": [ | |
| 1497 | + "billie_youtube", | |
| 1498 | + "titan_youtube_2022", | |
| 1499 | + "firecrawl_enrich" | |
| 1500 | + ], | |
| 1501 | + "total_followers": 488900, | |
| 1502 | + "platform_count": 2, | |
| 1503 | + "tier": "macro (100K+)" | |
| 1504 | + }, | |
| 1505 | + { | |
| 1506 | + "name": "Rosalie Vaillancourt", | |
| 1507 | + "handles": { | |
| 1508 | + "instagram": "rosalievaillancourt", | |
| 1509 | + "tiktok": "rosalievaillancourt", | |
| 1510 | + "youtube": null, | |
| 1511 | + "facebook": null, | |
| 1512 | + "x": null, | |
| 1513 | + "twitch": null | |
| 1514 | + }, | |
| 1515 | + "metrics": { | |
| 1516 | + "instagram_followers": 234900, | |
| 1517 | + "tiktok_followers": 251200, | |
| 1518 | + "youtube_subscribers": null, | |
| 1519 | + "facebook_followers": null, | |
| 1520 | + "x_followers": null, | |
| 1521 | + "twitch_followers": null | |
| 1522 | + }, | |
| 1523 | + "category": "humour", | |
| 1524 | + "location": "Montréal", | |
| 1525 | + "notes": "Top 20 Modash Montréal, rang 6; humoriste; engagement 2.74% | #9 humoriste QC: 636 359 abonnés cumulés (FB+IG+TikTok+X, 2024) | enrichi via recherche web 2026-08", | |
| 1526 | + "sources": [ | |
| 1527 | + "modash", | |
| 1528 | + "jdm_humoristes", | |
| 1529 | + "firecrawl_enrich" | |
| 1530 | + ], | |
| 1531 | + "total_followers": 486100, | |
| 1532 | + "platform_count": 2, | |
| 1533 | + "tier": "macro (100K+)" | |
| 1534 | + }, | |
| 1535 | + { | |
| 1536 | + "name": "PL Cloutier", | |
| 1537 | + "handles": { | |
| 1538 | + "instagram": "plcloutier", | |
| 1539 | + "tiktok": "plcloutier", | |
| 1540 | + "youtube": "pl cloutier", | |
| 1541 | + "facebook": null, | |
| 1542 | + "x": "plcloutier", | |
| 1543 | + "twitch": null | |
| 1544 | + }, | |
| 1545 | + "metrics": { | |
| 1546 | + "instagram_followers": 81000, | |
| 1547 | + "tiktok_followers": 55800, | |
| 1548 | + "youtube_subscribers": 280000, | |
| 1549 | + "facebook_followers": null, | |
| 1550 | + "x_followers": 41128, | |
| 1551 | + "twitch_followers": null | |
| 1552 | + }, | |
| 1553 | + "category": "humour", | |
| 1554 | + "location": "Montréal, Québec", | |
| 1555 | + "notes": "youtubeur; « règne sur YouTube Québec » selon le top 5 de L'Hebdo 2026 | Vidéos humoristiques et d'opinion. Chiffres 2022. | Icône YouTube Québec selon canadult; audience Québec francophone, revenus estimés 300k$–1M$. | enrichi via recherche web 2026-08", | |
| 1556 | + "sources": [ | |
| 1557 | + "lhebdo", | |
| 1558 | + "billie_youtube", | |
| 1559 | + "canadult", | |
| 1560 | + "firecrawl_enrich" | |
| 1561 | + ], | |
| 1562 | + "total_followers": 457928, | |
| 1563 | + "platform_count": 4, | |
| 1564 | + "tier": "macro (100K+)" | |
| 1565 | + }, | |
| 1566 | + { | |
| 1567 | + "name": "Lewis le fou", | |
| 1568 | + "handles": { | |
| 1569 | + "instagram": "lewislefou", | |
| 1570 | + "tiktok": "lewislefou", | |
| 1571 | + "youtube": "lewis le fou", | |
| 1572 | + "facebook": null, | |
| 1573 | + "x": null, | |
| 1574 | + "twitch": "lewislefou" | |
| 1575 | + }, | |
| 1576 | + "metrics": { | |
| 1577 | + "instagram_followers": 151000, | |
| 1578 | + "tiktok_followers": 260000, | |
| 1579 | + "youtube_subscribers": 44400, | |
| 1580 | + "facebook_followers": null, | |
| 1581 | + "x_followers": null, | |
| 1582 | + "twitch_followers": null | |
| 1583 | + }, | |
| 1584 | + "category": "humour", | |
| 1585 | + "location": "Charlevoix, Québec", | |
| 1586 | + "notes": "Divertissement spontané entre Twitch et YouTube. Top YouTubeurs QC 2026 (ONEWAY). | Streamer IRL #1 du top Twitch Québec 2026; subathon de 30 jours, records d'abonnements Twitch au Québec | enrichi via recherche web 2026-08", | |
| 1587 | + "sources": [ | |
| 1588 | + "oneway_youtube", | |
| 1589 | + "leregard_twitch", | |
| 1590 | + "firecrawl_enrich" | |
| 1591 | + ], | |
| 1592 | + "total_followers": 455400, | |
| 1593 | + "platform_count": 4, | |
| 1594 | + "tier": "macro (100K+)" | |
| 1595 | + }, | |
| 1596 | + { | |
| 1597 | + "name": "Frederike Lachance-Brulotte (Folks and Forks)", | |
| 1598 | + "handles": { | |
| 1599 | + "instagram": "folksandforks", | |
| 1600 | + "tiktok": null, | |
| 1601 | + "youtube": null, | |
| 1602 | + "facebook": null, | |
| 1603 | + "x": null, | |
| 1604 | + "twitch": null | |
| 1605 | + }, | |
| 1606 | + "metrics": { | |
| 1607 | + "instagram_followers": 179700, | |
| 1608 | + "tiktok_followers": 260900, | |
| 1609 | + "youtube_subscribers": null, | |
| 1610 | + "facebook_followers": null, | |
| 1611 | + "x_followers": null, | |
| 1612 | + "twitch_followers": null | |
| 1613 | + }, | |
| 1614 | + "category": "food", | |
| 1615 | + "location": "Québec", | |
| 1616 | + "notes": "Auteure Folks and Forks (2 tomes), prêts-à-manger chez IGA; TikTok 260 900 selon La Grappe", | |
| 1617 | + "sources": [ | |
| 1618 | + "feedspot_food", | |
| 1619 | + "lagrappe_food" | |
| 1620 | + ], | |
| 1621 | + "total_followers": 440600, | |
| 1622 | + "platform_count": 2, | |
| 1623 | + "tier": "macro (100K+)" | |
| 1624 | + }, | |
| 1625 | + { | |
| 1626 | + "name": "Jessie Gélinas", | |
| 1627 | + "handles": { | |
| 1628 | + "instagram": "jessiegelinas_", | |
| 1629 | + "tiktok": "jessiegelinas_", | |
| 1630 | + "youtube": "jessiieg999", | |
| 1631 | + "facebook": null, | |
| 1632 | + "x": null, | |
| 1633 | + "twitch": null | |
| 1634 | + }, | |
| 1635 | + "metrics": { | |
| 1636 | + "instagram_followers": 50800, | |
| 1637 | + "tiktok_followers": null, | |
| 1638 | + "youtube_subscribers": 355000, | |
| 1639 | + "facebook_followers": null, | |
| 1640 | + "x_followers": null, | |
| 1641 | + "twitch_followers": null | |
| 1642 | + }, | |
| 1643 | + "category": "lifestyle", | |
| 1644 | + "location": "Québec", | |
| 1645 | + "notes": "Mode, beauté, déco et humour; publie environ une fois par mois. Chiffres 2022. | enrichi via recherche web 2026-08", | |
| 1646 | + "sources": [ | |
| 1647 | + "billie_youtube", | |
| 1648 | + "firecrawl_enrich" | |
| 1649 | + ], | |
| 1650 | + "total_followers": 405800, | |
| 1651 | + "platform_count": 3, | |
| 1652 | + "tier": "macro (100K+)" | |
| 1653 | + }, | |
| 1654 | + { | |
| 1655 | + "name": "Rym Nebbak", | |
| 1656 | + "handles": { | |
| 1657 | + "instagram": "rymnebbak", | |
| 1658 | + "tiktok": "rymnebbak", | |
| 1659 | + "youtube": null, | |
| 1660 | + "facebook": null, | |
| 1661 | + "x": null, | |
| 1662 | + "twitch": null | |
| 1663 | + }, | |
| 1664 | + "metrics": { | |
| 1665 | + "instagram_followers": 220000, | |
| 1666 | + "tiktok_followers": 168900, | |
| 1667 | + "youtube_subscribers": null, | |
| 1668 | + "facebook_followers": null, | |
| 1669 | + "x_followers": null, | |
| 1670 | + "twitch_followers": null | |
| 1671 | + }, | |
| 1672 | + "category": "télé-réalité", | |
| 1673 | + "location": "Québec", | |
| 1674 | + "notes": "ex-Occupation Double Afrique du Sud | enrichi via recherche web 2026-08", | |
| 1675 | + "sources": [ | |
| 1676 | + "billie", | |
| 1677 | + "firecrawl_enrich" | |
| 1678 | + ], | |
| 1679 | + "total_followers": 388900, | |
| 1680 | + "platform_count": 2, | |
| 1681 | + "tier": "macro (100K+)" | |
| 1682 | + }, | |
| 1683 | + { | |
| 1684 | + "name": "Catherine Belle", | |
| 1685 | + "handles": { | |
| 1686 | + "instagram": "cath_belle", | |
| 1687 | + "tiktok": "cath.belle", | |
| 1688 | + "youtube": null, | |
| 1689 | + "facebook": null, | |
| 1690 | + "x": null, | |
| 1691 | + "twitch": null | |
| 1692 | + }, | |
| 1693 | + "metrics": { | |
| 1694 | + "instagram_followers": 368000, | |
| 1695 | + "tiktok_followers": 14100, | |
| 1696 | + "youtube_subscribers": null, | |
| 1697 | + "facebook_followers": null, | |
| 1698 | + "x_followers": null, | |
| 1699 | + "twitch_followers": null | |
| 1700 | + }, | |
| 1701 | + "category": "lifestyle", | |
| 1702 | + "location": "Québec", | |
| 1703 | + "notes": "contenu voyage et bien-être | enrichi via recherche web 2026-08", | |
| 1704 | + "sources": [ | |
| 1705 | + "billie", | |
| 1706 | + "firecrawl_enrich" | |
| 1707 | + ], | |
| 1708 | + "total_followers": 382100, | |
| 1709 | + "platform_count": 2, | |
| 1710 | + "tier": "macro (100K+)" | |
| 1711 | + }, | |
| 1712 | + { | |
| 1713 | + "name": "Dom Babin", | |
| 1714 | + "handles": { | |
| 1715 | + "instagram": "dombabin", | |
| 1716 | + "tiktok": "dominicbabinn", | |
| 1717 | + "youtube": null, | |
| 1718 | + "facebook": null, | |
| 1719 | + "x": null, | |
| 1720 | + "twitch": null | |
| 1721 | + }, | |
| 1722 | + "metrics": { | |
| 1723 | + "instagram_followers": 180600, | |
| 1724 | + "tiktok_followers": 197600, | |
| 1725 | + "youtube_subscribers": null, | |
| 1726 | + "facebook_followers": null, | |
| 1727 | + "x_followers": null, | |
| 1728 | + "twitch_followers": null | |
| 1729 | + }, | |
| 1730 | + "category": "humour", | |
| 1731 | + "location": "Montréal", | |
| 1732 | + "notes": "Top 20 Modash Montréal, rang 10; humoriste, spectacle « Américain du Nord français »; engagement 8.61% | enrichi via recherche web 2026-08", | |
| 1733 | + "sources": [ | |
| 1734 | + "modash", | |
| 1735 | + "firecrawl_enrich" | |
| 1736 | + ], | |
| 1737 | + "total_followers": 378200, | |
| 1738 | + "platform_count": 2, | |
| 1739 | + "tier": "macro (100K+)" | |
| 1740 | + }, | |
| 1741 | + { | |
| 1742 | + "name": "Alanis Désilets", | |
| 1743 | + "handles": { | |
| 1744 | + "instagram": "alanis.desilets", | |
| 1745 | + "tiktok": "alanisdesilets", | |
| 1746 | + "youtube": null, | |
| 1747 | + "facebook": "desiletsalanis", | |
| 1748 | + "x": null, | |
| 1749 | + "twitch": null | |
| 1750 | + }, | |
| 1751 | + "metrics": { | |
| 1752 | + "instagram_followers": 278000, | |
| 1753 | + "tiktok_followers": 89500, | |
| 1754 | + "youtube_subscribers": null, | |
| 1755 | + "facebook_followers": null, | |
| 1756 | + "x_followers": null, | |
| 1757 | + "twitch_followers": null | |
| 1758 | + }, | |
| 1759 | + "category": "entrepreneur", | |
| 1760 | + "location": "Montréal, QC", | |
| 1761 | + "notes": "plusieurs entreprises (La campagne d'ici, La Jasette), podcast « Faut qu'on s'appelle », maman | enrichi via recherche web 2026-08", | |
| 1762 | + "sources": [ | |
| 1763 | + "billie", | |
| 1764 | + "firecrawl_enrich" | |
| 1765 | + ], | |
| 1766 | + "total_followers": 367500, | |
| 1767 | + "platform_count": 3, | |
| 1768 | + "tier": "macro (100K+)" | |
| 1769 | + }, | |
| 1770 | + { | |
| 1771 | + "name": "Hey Raion", | |
| 1772 | + "handles": { | |
| 1773 | + "instagram": null, | |
| 1774 | + "tiktok": null, | |
| 1775 | + "youtube": null, | |
| 1776 | + "facebook": null, | |
| 1777 | + "x": null, | |
| 1778 | + "twitch": "heyraion" | |
| 1779 | + }, | |
| 1780 | + "metrics": { | |
| 1781 | + "instagram_followers": null, | |
| 1782 | + "tiktok_followers": 189000, | |
| 1783 | + "youtube_subscribers": 135000, | |
| 1784 | + "facebook_followers": null, | |
| 1785 | + "x_followers": null, | |
| 1786 | + "twitch_followers": 37900 | |
| 1787 | + }, | |
| 1788 | + "category": "gaming", | |
| 1789 | + "location": "Montréal", | |
| 1790 | + "notes": "Streamer gaming (gacha), culture japonaise/chinoise, fitness et tech", | |
| 1791 | + "sources": [ | |
| 1792 | + "collabstr_twitch" | |
| 1793 | + ], | |
| 1794 | + "total_followers": 361900, | |
| 1795 | + "platform_count": 3, | |
| 1796 | + "tier": "macro (100K+)" | |
| 1797 | + }, | |
| 1798 | + { | |
| 1799 | + "name": "Beatrice Bouchard", | |
| 1800 | + "handles": { | |
| 1801 | + "instagram": "beatricebouchard", | |
| 1802 | + "tiktok": "beatricebouchard001", | |
| 1803 | + "youtube": null, | |
| 1804 | + "facebook": null, | |
| 1805 | + "x": null, | |
| 1806 | + "twitch": null | |
| 1807 | + }, | |
| 1808 | + "metrics": { | |
| 1809 | + "instagram_followers": 359000, | |
| 1810 | + "tiktok_followers": 1554, | |
| 1811 | + "youtube_subscribers": null, | |
| 1812 | + "facebook_followers": null, | |
| 1813 | + "x_followers": null, | |
| 1814 | + "twitch_followers": null | |
| 1815 | + }, | |
| 1816 | + "category": "lifestyle", | |
| 1817 | + "location": "Montréal", | |
| 1818 | + "notes": "créatrice de contenu, sœur jumelle de la joueuse de tennis Eugenie Bouchard | enrichi via recherche web 2026-08", | |
| 1819 | + "sources": [ | |
| 1820 | + "billie", | |
| 1821 | + "firecrawl_enrich" | |
| 1822 | + ], | |
| 1823 | + "total_followers": 360554, | |
| 1824 | + "platform_count": 2, | |
| 1825 | + "tier": "macro (100K+)" | |
| 1826 | + }, | |
| 1827 | + { | |
| 1828 | + "name": "Carey Price", | |
| 1829 | + "handles": { | |
| 1830 | + "instagram": "cp0031", | |
| 1831 | + "tiktok": null, | |
| 1832 | + "youtube": null, | |
| 1833 | + "facebook": null, | |
| 1834 | + "x": null, | |
| 1835 | + "twitch": null | |
| 1836 | + }, | |
| 1837 | + "metrics": { | |
| 1838 | + "instagram_followers": 360500, | |
| 1839 | + "tiktok_followers": null, | |
| 1840 | + "youtube_subscribers": null, | |
| 1841 | + "facebook_followers": null, | |
| 1842 | + "x_followers": null, | |
| 1843 | + "twitch_followers": null | |
| 1844 | + }, | |
| 1845 | + "category": "sport", | |
| 1846 | + "location": "Montréal", | |
| 1847 | + "notes": "Top 20 Modash Montréal, rang 1; gardien de but des Canadiens de Montréal; engagement 3.22%", | |
| 1848 | + "sources": [ | |
| 1849 | + "modash" | |
| 1850 | + ], | |
| 1851 | + "total_followers": 360500, | |
| 1852 | + "platform_count": 1, | |
| 1853 | + "tier": "macro (100K+)" | |
| 1854 | + }, | |
| 1855 | + { | |
| 1856 | + "name": "Marilou Bélanger", | |
| 1857 | + "handles": { | |
| 1858 | + "instagram": "mariloublg_", | |
| 1859 | + "tiktok": "mariloublg_", | |
| 1860 | + "youtube": null, | |
| 1861 | + "facebook": null, | |
| 1862 | + "x": null, | |
| 1863 | + "twitch": null | |
| 1864 | + }, | |
| 1865 | + "metrics": { | |
| 1866 | + "instagram_followers": 262000, | |
| 1867 | + "tiktok_followers": 94200, | |
| 1868 | + "youtube_subscribers": null, | |
| 1869 | + "facebook_followers": null, | |
| 1870 | + "x_followers": null, | |
| 1871 | + "twitch_followers": null | |
| 1872 | + }, | |
| 1873 | + "category": "télé-réalité", | |
| 1874 | + "location": "Montréal", | |
| 1875 | + "notes": "mannequin, popularité après Occupation Double dans l'Ouest | enrichi via recherche web 2026-08", | |
| 1876 | + "sources": [ | |
| 1877 | + "billie", | |
| 1878 | + "firecrawl_enrich" | |
| 1879 | + ], | |
| 1880 | + "total_followers": 356200, | |
| 1881 | + "platform_count": 2, | |
| 1882 | + "tier": "macro (100K+)" | |
| 1883 | + }, | |
| 1884 | + { | |
| 1885 | + "name": "Lisa Homsy", | |
| 1886 | + "handles": { | |
| 1887 | + "instagram": "lisahomsy", | |
| 1888 | + "tiktok": "lisahomsy_", | |
| 1889 | + "youtube": null, | |
| 1890 | + "facebook": "homsy.lisa", | |
| 1891 | + "x": "lisahomsy", | |
| 1892 | + "twitch": null | |
| 1893 | + }, | |
| 1894 | + "metrics": { | |
| 1895 | + "instagram_followers": 296000, | |
| 1896 | + "tiktok_followers": 59300, | |
| 1897 | + "youtube_subscribers": null, | |
| 1898 | + "facebook_followers": null, | |
| 1899 | + "x_followers": null, | |
| 1900 | + "twitch_followers": null | |
| 1901 | + }, | |
| 1902 | + "category": "voyage", | |
| 1903 | + "location": "Montréal", | |
| 1904 | + "notes": "photos de voyage, publie en anglais mais bilingue | enrichi via recherche web 2026-08", | |
| 1905 | + "sources": [ | |
| 1906 | + "billie", | |
| 1907 | + "firecrawl_enrich" | |
| 1908 | + ], | |
| 1909 | + "total_followers": 355300, | |
| 1910 | + "platform_count": 4, | |
| 1911 | + "tier": "macro (100K+)" | |
| 1912 | + }, | |
| 1913 | + { | |
| 1914 | + "name": "Nick Faber", | |
| 1915 | + "handles": { | |
| 1916 | + "instagram": "nickfaberr", | |
| 1917 | + "tiktok": null, | |
| 1918 | + "youtube": null, | |
| 1919 | + "facebook": null, | |
| 1920 | + "x": null, | |
| 1921 | + "twitch": null | |
| 1922 | + }, | |
| 1923 | + "metrics": { | |
| 1924 | + "instagram_followers": 198000, | |
| 1925 | + "tiktok_followers": 153000, | |
| 1926 | + "youtube_subscribers": null, | |
| 1927 | + "facebook_followers": null, | |
| 1928 | + "x_followers": null, | |
| 1929 | + "twitch_followers": null | |
| 1930 | + }, | |
| 1931 | + "category": "food", | |
| 1932 | + "location": "Montréal", | |
| 1933 | + "notes": "#1 du top 7 influenceurs food La Grappe 2026; découvertes de restos montréalais", | |
| 1934 | + "sources": [ | |
| 1935 | + "lagrappe_food" | |
| 1936 | + ], | |
| 1937 | + "total_followers": 351000, | |
| 1938 | + "platform_count": 2, | |
| 1939 | + "tier": "macro (100K+)" | |
| 1940 | + }, | |
| 1941 | + { | |
| 1942 | + "name": "Brendan Mikan", | |
| 1943 | + "handles": { | |
| 1944 | + "instagram": "brendanmikan", | |
| 1945 | + "tiktok": "brendanmikan", | |
| 1946 | + "youtube": null, | |
| 1947 | + "facebook": null, | |
| 1948 | + "x": null, | |
| 1949 | + "twitch": null | |
| 1950 | + }, | |
| 1951 | + "metrics": { | |
| 1952 | + "instagram_followers": 152600, | |
| 1953 | + "tiktok_followers": 173900, | |
| 1954 | + "youtube_subscribers": null, | |
| 1955 | + "facebook_followers": null, | |
| 1956 | + "x_followers": null, | |
| 1957 | + "twitch_followers": null | |
| 1958 | + }, | |
| 1959 | + "category": "lifestyle", | |
| 1960 | + "location": "Montréal", | |
| 1961 | + "notes": "Top 20 Modash Montréal, rang 14; reviewer et foodie (agence Dulcedo); engagement 2.04% | enrichi via recherche web 2026-08", | |
| 1962 | + "sources": [ | |
| 1963 | + "modash", | |
| 1964 | + "firecrawl_enrich" | |
| 1965 | + ], | |
| 1966 | + "total_followers": 326500, | |
| 1967 | + "platform_count": 2, | |
| 1968 | + "tier": "macro (100K+)" | |
| 1969 | + }, | |
| 1970 | + { | |
| 1971 | + "name": "Catherine Paquin", | |
| 1972 | + "handles": { | |
| 1973 | + "instagram": "catherinepeachh", | |
| 1974 | + "tiktok": null, | |
| 1975 | + "youtube": null, | |
| 1976 | + "facebook": null, | |
| 1977 | + "x": null, | |
| 1978 | + "twitch": null | |
| 1979 | + }, | |
| 1980 | + "metrics": { | |
| 1981 | + "instagram_followers": 323000, | |
| 1982 | + "tiktok_followers": null, | |
| 1983 | + "youtube_subscribers": null, | |
| 1984 | + "facebook_followers": null, | |
| 1985 | + "x_followers": null, | |
| 1986 | + "twitch_followers": null | |
| 1987 | + }, | |
| 1988 | + "category": "télé-réalité", | |
| 1989 | + "location": "Québec", | |
| 1990 | + "notes": "« Peach », ex-OD; gamme de bijoux Appy | enrichi via recherche web 2026-08", | |
| 1991 | + "sources": [ | |
| 1992 | + "billie", | |
| 1993 | + "firecrawl_enrich" | |
| 1994 | + ], | |
| 1995 | + "total_followers": 323000, | |
| 1996 | + "platform_count": 1, | |
| 1997 | + "tier": "macro (100K+)" | |
| 1998 | + }, | |
| 1999 | + { | |
| 2000 | + "name": "Milaydie Bujold", | |
| 2001 | + "handles": { | |
| 2002 | + "instagram": "milaydie", | |
| 2003 | + "tiktok": "milaydiebujold", | |
| 2004 | + "youtube": null, | |
| 2005 | + "facebook": null, | |
| 2006 | + "x": null, | |
| 2007 | + "twitch": null | |
| 2008 | + }, | |
| 2009 | + "metrics": { | |
| 2010 | + "instagram_followers": 263000, | |
| 2011 | + "tiktok_followers": 58000, | |
| 2012 | + "youtube_subscribers": null, | |
| 2013 | + "facebook_followers": null, | |
| 2014 | + "x_followers": null, | |
| 2015 | + "twitch_followers": null | |
| 2016 | + }, | |
| 2017 | + "category": "lifestyle", | |
| 2018 | + "location": "Québec", | |
| 2019 | + "notes": "looks mode, voyages et quotidien avec ses chats | enrichi via recherche web 2026-08", | |
| 2020 | + "sources": [ | |
| 2021 | + "billie", | |
| 2022 | + "firecrawl_enrich" | |
| 2023 | + ], | |
| 2024 | + "total_followers": 321000, | |
| 2025 | + "platform_count": 2, | |
| 2026 | + "tier": "macro (100K+)" | |
| 2027 | + }, | |
| 2028 | + { | |
| 2029 | + "name": "Mathieu Dufour (Math Duff)", | |
| 2030 | + "handles": { | |
| 2031 | + "instagram": "mathduff", | |
| 2032 | + "tiktok": "mathduff", | |
| 2033 | + "youtube": "mathduff.mathieudufour", | |
| 2034 | + "facebook": "mathduff", | |
| 2035 | + "x": null, | |
| 2036 | + "twitch": null | |
| 2037 | + }, | |
| 2038 | + "metrics": { | |
| 2039 | + "instagram_followers": 304500, | |
| 2040 | + "tiktok_followers": null, | |
| 2041 | + "youtube_subscribers": null, | |
| 2042 | + "facebook_followers": null, | |
| 2043 | + "x_followers": null, | |
| 2044 | + "twitch_followers": null | |
| 2045 | + }, | |
| 2046 | + "category": "humour", | |
| 2047 | + "location": "Montréal", | |
| 2048 | + "notes": "Humoriste; gagnant Gala InfluenceCréation 2025 — Humour & divertissement. | Top 20 Modash Montréal, rang 2; humoriste (Math Duff); engagement 9.6% | #12 humoriste QC: 572 347 abonnés cumulés (FB+IG+TikTok+X, 2024) | enrichi via recherche web 2026-08; handles seulement, aucun compte d'abonnés trouvé", | |
| 2049 | + "sources": [ | |
| 2050 | + "billie_gala", | |
| 2051 | + "grenier_gala", | |
| 2052 | + "modash", | |
| 2053 | + "jdm_humoristes", | |
| 2054 | + "firecrawl_enrich" | |
| 2055 | + ], | |
| 2056 | + "total_followers": 304500, | |
| 2057 | + "platform_count": 4, | |
| 2058 | + "tier": "macro (100K+)" | |
| 2059 | + }, | |
| 2060 | + { | |
| 2061 | + "name": "Marilou Bourdon (Trois fois par jour)", | |
| 2062 | + "handles": { | |
| 2063 | + "instagram": "troisfoisparjour", | |
| 2064 | + "tiktok": null, | |
| 2065 | + "youtube": null, | |
| 2066 | + "facebook": null, | |
| 2067 | + "x": null, | |
| 2068 | + "twitch": null | |
| 2069 | + }, | |
| 2070 | + "metrics": { | |
| 2071 | + "instagram_followers": 287000, | |
| 2072 | + "tiktok_followers": null, | |
| 2073 | + "youtube_subscribers": null, | |
| 2074 | + "facebook_followers": null, | |
| 2075 | + "x_followers": null, | |
| 2076 | + "twitch_followers": null | |
| 2077 | + }, | |
| 2078 | + "category": "food", | |
| 2079 | + "location": "Québec", | |
| 2080 | + "notes": "Star de la cuisine au Québec; livres Trois fois par jour (tomes 1-2 + Desserts), boutique en ligne; chiffres de 2019", | |
| 2081 | + "sources": [ | |
| 2082 | + "clark_food" | |
| 2083 | + ], | |
| 2084 | + "total_followers": 287000, | |
| 2085 | + "platform_count": 1, | |
| 2086 | + "tier": "macro (100K+)" | |
| 2087 | + }, | |
| 2088 | + { | |
| 2089 | + "name": "Emy-Jade Greaves", | |
| 2090 | + "handles": { | |
| 2091 | + "instagram": "emyjade.g", | |
| 2092 | + "tiktok": "emyjade.g", | |
| 2093 | + "youtube": null, | |
| 2094 | + "facebook": null, | |
| 2095 | + "x": null, | |
| 2096 | + "twitch": null | |
| 2097 | + }, | |
| 2098 | + "metrics": { | |
| 2099 | + "instagram_followers": 208000, | |
| 2100 | + "tiktok_followers": 45100, | |
| 2101 | + "youtube_subscribers": 32700, | |
| 2102 | + "facebook_followers": null, | |
| 2103 | + "x_followers": null, | |
| 2104 | + "twitch_followers": null | |
| 2105 | + }, | |
| 2106 | + "category": "fitness", | |
| 2107 | + "location": "Montréal, QC", | |
| 2108 | + "notes": "prof de yoga, marque de loungewear Reckless Minds | Créatrice de contenu adulte (OnlyFans), populaire sur Instagram/TikTok; revenus estimés 100k$–300k$ (canadult). | enrichi via recherche web 2026-08", | |
| 2109 | + "sources": [ | |
| 2110 | + "billie", | |
| 2111 | + "canadult", | |
| 2112 | + "firecrawl_enrich" | |
| 2113 | + ], | |
| 2114 | + "total_followers": 285800, | |
| 2115 | + "platform_count": 3, | |
| 2116 | + "tier": "macro (100K+)" | |
| 2117 | + }, | |
| 2118 | + { | |
| 2119 | + "name": "Esther Brzezinski", | |
| 2120 | + "handles": { | |
| 2121 | + "instagram": "estieb", | |
| 2122 | + "tiktok": "estherbrzezinski", | |
| 2123 | + "youtube": null, | |
| 2124 | + "facebook": null, | |
| 2125 | + "x": "estieb", | |
| 2126 | + "twitch": null | |
| 2127 | + }, | |
| 2128 | + "metrics": { | |
| 2129 | + "instagram_followers": 108200, | |
| 2130 | + "tiktok_followers": 165400, | |
| 2131 | + "youtube_subscribers": null, | |
| 2132 | + "facebook_followers": null, | |
| 2133 | + "x_followers": null, | |
| 2134 | + "twitch_followers": null | |
| 2135 | + }, | |
| 2136 | + "category": "lifestyle", | |
| 2137 | + "location": "Montréal", | |
| 2138 | + "notes": "« Age Is Not My Cage », actrice/voix/podcasteuse de 65 ans, engagement 10% | enrichi via recherche web 2026-08", | |
| 2139 | + "sources": [ | |
| 2140 | + "collabstr", | |
| 2141 | + "firecrawl_enrich" | |
| 2142 | + ], | |
| 2143 | + "total_followers": 273600, | |
| 2144 | + "platform_count": 3, | |
| 2145 | + "tier": "macro (100K+)" | |
| 2146 | + }, | |
| 2147 | + { | |
| 2148 | + "name": "Amélie Barbeau", | |
| 2149 | + "handles": { | |
| 2150 | + "instagram": "amelie.barbeau", | |
| 2151 | + "tiktok": null, | |
| 2152 | + "youtube": "amyfashionimpact", | |
| 2153 | + "facebook": null, | |
| 2154 | + "x": null, | |
| 2155 | + "twitch": null | |
| 2156 | + }, | |
| 2157 | + "metrics": { | |
| 2158 | + "instagram_followers": 59200, | |
| 2159 | + "tiktok_followers": 8540, | |
| 2160 | + "youtube_subscribers": 202000, | |
| 2161 | + "facebook_followers": null, | |
| 2162 | + "x_followers": null, | |
| 2163 | + "twitch_followers": null | |
| 2164 | + }, | |
| 2165 | + "category": "humour", | |
| 2166 | + "location": "Québec", | |
| 2167 | + "notes": "DIY et vidéos humoristiques, plus de 10 ans sur YouTube. Chiffres 2022. Aussi #44 (beauté/mode) au top 100 Titan 2022. | enrichi via recherche web 2026-08", | |
| 2168 | + "sources": [ | |
| 2169 | + "billie_youtube", | |
| 2170 | + "titan_youtube_2022", | |
| 2171 | + "firecrawl_enrich" | |
| 2172 | + ], | |
| 2173 | + "total_followers": 269740, | |
| 2174 | + "platform_count": 3, | |
| 2175 | + "tier": "macro (100K+)" | |
| 2176 | + }, | |
| 2177 | + { | |
| 2178 | + "name": "Julie-Anne Ho", | |
| 2179 | + "handles": { | |
| 2180 | + "instagram": "julieanneho", | |
| 2181 | + "tiktok": "julieanneho", | |
| 2182 | + "youtube": null, | |
| 2183 | + "facebook": null, | |
| 2184 | + "x": null, | |
| 2185 | + "twitch": null | |
| 2186 | + }, | |
| 2187 | + "metrics": { | |
| 2188 | + "instagram_followers": 237000, | |
| 2189 | + "tiktok_followers": 30900, | |
| 2190 | + "youtube_subscribers": null, | |
| 2191 | + "facebook_followers": null, | |
| 2192 | + "x_followers": null, | |
| 2193 | + "twitch_followers": null | |
| 2194 | + }, | |
| 2195 | + "category": "mode", | |
| 2196 | + "location": "Montréal", | |
| 2197 | + "notes": "ex-OD Grèce et OD Chez Nous; marque d'accessoires Essaim | Créatrice de contenu ayant lancé la ligne d'accessoires Essaim; mentionnée dans un article connexe de Billie (aucune métrique dans la source). | enrichi via recherche web 2026-08", | |
| 2198 | + "sources": [ | |
| 2199 | + "billie", | |
| 2200 | + "billie_youtube", | |
| 2201 | + "firecrawl_enrich" | |
| 2202 | + ], | |
| 2203 | + "total_followers": 267900, | |
| 2204 | + "platform_count": 2, | |
| 2205 | + "tier": "macro (100K+)" | |
| 2206 | + }, | |
| 2207 | + { | |
| 2208 | + "name": "Nick Suzuki", | |
| 2209 | + "handles": { | |
| 2210 | + "instagram": "nsuzuki_37", | |
| 2211 | + "tiktok": null, | |
| 2212 | + "youtube": null, | |
| 2213 | + "facebook": null, | |
| 2214 | + "x": null, | |
| 2215 | + "twitch": null | |
| 2216 | + }, | |
| 2217 | + "metrics": { | |
| 2218 | + "instagram_followers": 258000, | |
| 2219 | + "tiktok_followers": null, | |
| 2220 | + "youtube_subscribers": null, | |
| 2221 | + "facebook_followers": null, | |
| 2222 | + "x_followers": null, | |
| 2223 | + "twitch_followers": null | |
| 2224 | + }, | |
| 2225 | + "category": "sport", | |
| 2226 | + "location": "Montréal", | |
| 2227 | + "notes": "Top 20 Modash Montréal, rang 5; capitaine des Canadiens de Montréal; engagement 3% | enrichi via recherche web 2026-08", | |
| 2228 | + "sources": [ | |
| 2229 | + "modash", | |
| 2230 | + "firecrawl_enrich" | |
| 2231 | + ], | |
| 2232 | + "total_followers": 258000, | |
| 2233 | + "platform_count": 1, | |
| 2234 | + "tier": "macro (100K+)" | |
| 2235 | + }, | |
| 2236 | + { | |
| 2237 | + "name": "Christine Morency", | |
| 2238 | + "handles": { | |
| 2239 | + "instagram": "morenclic", | |
| 2240 | + "tiktok": "christinemorency", | |
| 2241 | + "youtube": "christinemorencycm", | |
| 2242 | + "facebook": null, | |
| 2243 | + "x": null, | |
| 2244 | + "twitch": null | |
| 2245 | + }, | |
| 2246 | + "metrics": { | |
| 2247 | + "instagram_followers": 156500, | |
| 2248 | + "tiktok_followers": 93400, | |
| 2249 | + "youtube_subscribers": 2430, | |
| 2250 | + "facebook_followers": null, | |
| 2251 | + "x_followers": null, | |
| 2252 | + "twitch_followers": null | |
| 2253 | + }, | |
| 2254 | + "category": "humour", | |
| 2255 | + "location": "Montréal", | |
| 2256 | + "notes": "Top 20 Modash Montréal, rang 13; humoriste à temps plein; engagement 4.78% | enrichi via recherche web 2026-08", | |
| 2257 | + "sources": [ | |
| 2258 | + "modash", | |
| 2259 | + "firecrawl_enrich" | |
| 2260 | + ], | |
| 2261 | + "total_followers": 252330, | |
| 2262 | + "platform_count": 3, | |
| 2263 | + "tier": "macro (100K+)" | |
| 2264 | + }, | |
| 2265 | + { | |
| 2266 | + "name": "Maude Lavoie", | |
| 2267 | + "handles": { | |
| 2268 | + "instagram": "maudelavoie", | |
| 2269 | + "tiktok": "maude.lavoie", | |
| 2270 | + "youtube": null, | |
| 2271 | + "facebook": null, | |
| 2272 | + "x": null, | |
| 2273 | + "twitch": null | |
| 2274 | + }, | |
| 2275 | + "metrics": { | |
| 2276 | + "instagram_followers": 207000, | |
| 2277 | + "tiktok_followers": 34300, | |
| 2278 | + "youtube_subscribers": null, | |
| 2279 | + "facebook_followers": null, | |
| 2280 | + "x_followers": null, | |
| 2281 | + "twitch_followers": null | |
| 2282 | + }, | |
| 2283 | + "category": "télé-réalité", | |
| 2284 | + "location": "Québec", | |
| 2285 | + "notes": "ex-candidate OD Grèce, maintenant maman (embed plus récent: 188K) | enrichi via recherche web 2026-08", | |
| 2286 | + "sources": [ | |
| 2287 | + "billie", | |
| 2288 | + "firecrawl_enrich" | |
| 2289 | + ], | |
| 2290 | + "total_followers": 241300, | |
| 2291 | + "platform_count": 2, | |
| 2292 | + "tier": "macro (100K+)" | |
| 2293 | + }, | |
| 2294 | + { | |
| 2295 | + "name": "Myriam Lemay", | |
| 2296 | + "handles": { | |
| 2297 | + "instagram": "myriamlemaay", | |
| 2298 | + "tiktok": null, | |
| 2299 | + "youtube": null, | |
| 2300 | + "facebook": null, | |
| 2301 | + "x": null, | |
| 2302 | + "twitch": null | |
| 2303 | + }, | |
| 2304 | + "metrics": { | |
| 2305 | + "instagram_followers": 219000, | |
| 2306 | + "tiktok_followers": null, | |
| 2307 | + "youtube_subscribers": null, | |
| 2308 | + "facebook_followers": null, | |
| 2309 | + "x_followers": null, | |
| 2310 | + "twitch_followers": null | |
| 2311 | + }, | |
| 2312 | + "category": "lifestyle", | |
| 2313 | + "location": "Québec (ville)", | |
| 2314 | + "notes": "créatrice de contenu, maman depuis octobre 2021 | enrichi via recherche web 2026-08", | |
| 2315 | + "sources": [ | |
| 2316 | + "billie", | |
| 2317 | + "firecrawl_enrich" | |
| 2318 | + ], | |
| 2319 | + "total_followers": 219000, | |
| 2320 | + "platform_count": 1, | |
| 2321 | + "tier": "macro (100K+)" | |
| 2322 | + }, | |
| 2323 | + { | |
| 2324 | + "name": "Marina Bastarache", | |
| 2325 | + "handles": { | |
| 2326 | + "instagram": "marinabastarache", | |
| 2327 | + "tiktok": null, | |
| 2328 | + "youtube": null, | |
| 2329 | + "facebook": null, | |
| 2330 | + "x": null, | |
| 2331 | + "twitch": null | |
| 2332 | + }, | |
| 2333 | + "metrics": { | |
| 2334 | + "instagram_followers": 210000, | |
| 2335 | + "tiktok_followers": null, | |
| 2336 | + "youtube_subscribers": null, | |
| 2337 | + "facebook_followers": null, | |
| 2338 | + "x_followers": null, | |
| 2339 | + "twitch_followers": null | |
| 2340 | + }, | |
| 2341 | + "category": "voyage", | |
| 2342 | + "location": "Québec", | |
| 2343 | + "notes": "Comédienne et grande voyageuse (liste NoovoMoi des Québécois qui voyagent)", | |
| 2344 | + "sources": [ | |
| 2345 | + "noovomoi_voyage" | |
| 2346 | + ], | |
| 2347 | + "total_followers": 210000, | |
| 2348 | + "platform_count": 1, | |
| 2349 | + "tier": "macro (100K+)" | |
| 2350 | + }, | |
| 2351 | + { | |
| 2352 | + "name": "Émilie Rossignol", | |
| 2353 | + "handles": { | |
| 2354 | + "instagram": "emilierossignolarts", | |
| 2355 | + "tiktok": null, | |
| 2356 | + "youtube": null, | |
| 2357 | + "facebook": null, | |
| 2358 | + "x": null, | |
| 2359 | + "twitch": null | |
| 2360 | + }, | |
| 2361 | + "metrics": { | |
| 2362 | + "instagram_followers": 112200, | |
| 2363 | + "tiktok_followers": 97000, | |
| 2364 | + "youtube_subscribers": null, | |
| 2365 | + "facebook_followers": null, | |
| 2366 | + "x_followers": null, | |
| 2367 | + "twitch_followers": null | |
| 2368 | + }, | |
| 2369 | + "category": "food", | |
| 2370 | + "location": "Montréal", | |
| 2371 | + "notes": "Créatrice food, connexion et voyage; TikTok 97 000 selon La Grappe", | |
| 2372 | + "sources": [ | |
| 2373 | + "feedspot_food", | |
| 2374 | + "lagrappe_food" | |
| 2375 | + ], | |
| 2376 | + "total_followers": 209200, | |
| 2377 | + "platform_count": 2, | |
| 2378 | + "tier": "macro (100K+)" | |
| 2379 | + }, | |
| 2380 | + { | |
| 2381 | + "name": "Célin Do (Crazy Broke Asian)", | |
| 2382 | + "handles": { | |
| 2383 | + "instagram": "crazybr0keasian", | |
| 2384 | + "tiktok": null, | |
| 2385 | + "youtube": "crazybrokeasian", | |
| 2386 | + "facebook": null, | |
| 2387 | + "x": null, | |
| 2388 | + "twitch": null | |
| 2389 | + }, | |
| 2390 | + "metrics": { | |
| 2391 | + "instagram_followers": 208600, | |
| 2392 | + "tiktok_followers": null, | |
| 2393 | + "youtube_subscribers": null, | |
| 2394 | + "facebook_followers": null, | |
| 2395 | + "x_followers": null, | |
| 2396 | + "twitch_followers": null | |
| 2397 | + }, | |
| 2398 | + "category": "autre", | |
| 2399 | + "location": "Montréal", | |
| 2400 | + "notes": "Top 20 Modash Montréal, rang 7; docu-série et animation Crazy Broke Asian; engagement 2.43% | enrichi via recherche web 2026-08", | |
| 2401 | + "sources": [ | |
| 2402 | + "modash", | |
| 2403 | + "firecrawl_enrich" | |
| 2404 | + ], | |
| 2405 | + "total_followers": 208600, | |
| 2406 | + "platform_count": 2, | |
| 2407 | + "tier": "macro (100K+)" | |
| 2408 | + }, | |
| 2409 | + { | |
| 2410 | + "name": "Lane Hutson", | |
| 2411 | + "handles": { | |
| 2412 | + "instagram": "lanehutson_", | |
| 2413 | + "tiktok": null, | |
| 2414 | + "youtube": null, | |
| 2415 | + "facebook": null, | |
| 2416 | + "x": null, | |
| 2417 | + "twitch": null | |
| 2418 | + }, | |
| 2419 | + "metrics": { | |
| 2420 | + "instagram_followers": 196500, | |
| 2421 | + "tiktok_followers": null, | |
| 2422 | + "youtube_subscribers": null, | |
| 2423 | + "facebook_followers": null, | |
| 2424 | + "x_followers": null, | |
| 2425 | + "twitch_followers": null | |
| 2426 | + }, | |
| 2427 | + "category": "sport", | |
| 2428 | + "location": "Montréal", | |
| 2429 | + "notes": "Top 20 Modash Montréal, rang 8; défenseur des Canadiens de Montréal; engagement 3.3%", | |
| 2430 | + "sources": [ | |
| 2431 | + "modash" | |
| 2432 | + ], | |
| 2433 | + "total_followers": 196500, | |
| 2434 | + "platform_count": 1, | |
| 2435 | + "tier": "macro (100K+)" | |
| 2436 | + }, | |
| 2437 | + { | |
| 2438 | + "name": "Vanessa Larche", | |
| 2439 | + "handles": { | |
| 2440 | + "instagram": "vanessa.andrea.beauty", | |
| 2441 | + "tiktok": null, | |
| 2442 | + "youtube": null, | |
| 2443 | + "facebook": null, | |
| 2444 | + "x": null, | |
| 2445 | + "twitch": null | |
| 2446 | + }, | |
| 2447 | + "metrics": { | |
| 2448 | + "instagram_followers": 178000, | |
| 2449 | + "tiktok_followers": null, | |
| 2450 | + "youtube_subscribers": null, | |
| 2451 | + "facebook_followers": null, | |
| 2452 | + "x_followers": null, | |
| 2453 | + "twitch_followers": null | |
| 2454 | + }, | |
| 2455 | + "category": "beauté", | |
| 2456 | + "location": "Montréal", | |
| 2457 | + "notes": "beauté et lifestyle, maman de 2 garçons; 178K sur le compte embed @vanessa.larche | enrichi via recherche web 2026-08", | |
| 2458 | + "sources": [ | |
| 2459 | + "embold", | |
| 2460 | + "firecrawl_enrich" | |
| 2461 | + ], | |
| 2462 | + "total_followers": 178000, | |
| 2463 | + "platform_count": 1, | |
| 2464 | + "tier": "macro (100K+)" | |
| 2465 | + }, | |
| 2466 | + { | |
| 2467 | + "name": "Ivan Demidov", | |
| 2468 | + "handles": { | |
| 2469 | + "instagram": "_i.demidov", | |
| 2470 | + "tiktok": null, | |
| 2471 | + "youtube": null, | |
| 2472 | + "facebook": null, | |
| 2473 | + "x": null, | |
| 2474 | + "twitch": null | |
| 2475 | + }, | |
| 2476 | + "metrics": { | |
| 2477 | + "instagram_followers": 159400, | |
| 2478 | + "tiktok_followers": null, | |
| 2479 | + "youtube_subscribers": null, | |
| 2480 | + "facebook_followers": null, | |
| 2481 | + "x_followers": null, | |
| 2482 | + "twitch_followers": null | |
| 2483 | + }, | |
| 2484 | + "category": "sport", | |
| 2485 | + "location": "Montréal", | |
| 2486 | + "notes": "Top 20 Modash Montréal, rang 11; attaquant des Canadiens de Montréal (#93); engagement 12.39%", | |
| 2487 | + "sources": [ | |
| 2488 | + "modash" | |
| 2489 | + ], | |
| 2490 | + "total_followers": 159400, | |
| 2491 | + "platform_count": 1, | |
| 2492 | + "tier": "macro (100K+)" | |
| 2493 | + }, | |
| 2494 | + { | |
| 2495 | + "name": "Shamit Shome", | |
| 2496 | + "handles": { | |
| 2497 | + "instagram": "shamit_shome07", | |
| 2498 | + "tiktok": null, | |
| 2499 | + "youtube": null, | |
| 2500 | + "facebook": null, | |
| 2501 | + "x": null, | |
| 2502 | + "twitch": null | |
| 2503 | + }, | |
| 2504 | + "metrics": { | |
| 2505 | + "instagram_followers": 157400, | |
| 2506 | + "tiktok_followers": null, | |
| 2507 | + "youtube_subscribers": null, | |
| 2508 | + "facebook_followers": null, | |
| 2509 | + "x_followers": null, | |
| 2510 | + "twitch_followers": null | |
| 2511 | + }, | |
| 2512 | + "category": "sport", | |
| 2513 | + "location": "Montréal", | |
| 2514 | + "notes": "Top 20 Modash Montréal, rang 12; footballeur (soccer), audience surtout Montréal/Edmonton; engagement 17.77% | enrichi via recherche web 2026-08", | |
| 2515 | + "sources": [ | |
| 2516 | + "modash", | |
| 2517 | + "firecrawl_enrich" | |
| 2518 | + ], | |
| 2519 | + "total_followers": 157400, | |
| 2520 | + "platform_count": 1, | |
| 2521 | + "tier": "macro (100K+)" | |
| 2522 | + }, | |
| 2523 | + { | |
| 2524 | + "name": "Math Pellerin", | |
| 2525 | + "handles": { | |
| 2526 | + "instagram": "mathieupellerin._", | |
| 2527 | + "tiktok": null, | |
| 2528 | + "youtube": null, | |
| 2529 | + "facebook": null, | |
| 2530 | + "x": null, | |
| 2531 | + "twitch": null | |
| 2532 | + }, | |
| 2533 | + "metrics": { | |
| 2534 | + "instagram_followers": 155000, | |
| 2535 | + "tiktok_followers": null, | |
| 2536 | + "youtube_subscribers": null, | |
| 2537 | + "facebook_followers": null, | |
| 2538 | + "x_followers": null, | |
| 2539 | + "twitch_followers": null | |
| 2540 | + }, | |
| 2541 | + "category": "gaming", | |
| 2542 | + "location": "Québec", | |
| 2543 | + "notes": "Finaliste Occupation Double Afrique du Sud 2019, devenu streamer Twitch (gaming, discussions)", | |
| 2544 | + "sources": [ | |
| 2545 | + "leregard_twitch" | |
| 2546 | + ], | |
| 2547 | + "total_followers": 155000, | |
| 2548 | + "platform_count": 1, | |
| 2549 | + "tier": "macro (100K+)" | |
| 2550 | + }, | |
| 2551 | + { | |
| 2552 | + "name": "Félix-Antoine Tremblay", | |
| 2553 | + "handles": { | |
| 2554 | + "instagram": "phoenixantoine", | |
| 2555 | + "tiktok": null, | |
| 2556 | + "youtube": null, | |
| 2557 | + "facebook": null, | |
| 2558 | + "x": null, | |
| 2559 | + "twitch": null | |
| 2560 | + }, | |
| 2561 | + "metrics": { | |
| 2562 | + "instagram_followers": 151900, | |
| 2563 | + "tiktok_followers": null, | |
| 2564 | + "youtube_subscribers": null, | |
| 2565 | + "facebook_followers": null, | |
| 2566 | + "x_followers": null, | |
| 2567 | + "twitch_followers": null | |
| 2568 | + }, | |
| 2569 | + "category": "autre", | |
| 2570 | + "location": "Montréal", | |
| 2571 | + "notes": "Top 20 Modash Montréal, rang 15; comédien / créateur de contenu; engagement 2.32%", | |
| 2572 | + "sources": [ | |
| 2573 | + "modash" | |
| 2574 | + ], | |
| 2575 | + "total_followers": 151900, | |
| 2576 | + "platform_count": 1, | |
| 2577 | + "tier": "macro (100K+)" | |
| 2578 | + }, | |
| 2579 | + { | |
| 2580 | + "name": "Murphy Cooper", | |
| 2581 | + "handles": { | |
| 2582 | + "instagram": "murphycooper", | |
| 2583 | + "tiktok": null, | |
| 2584 | + "youtube": null, | |
| 2585 | + "facebook": null, | |
| 2586 | + "x": null, | |
| 2587 | + "twitch": null | |
| 2588 | + }, | |
| 2589 | + "metrics": { | |
| 2590 | + "instagram_followers": 144200, | |
| 2591 | + "tiktok_followers": null, | |
| 2592 | + "youtube_subscribers": null, | |
| 2593 | + "facebook_followers": null, | |
| 2594 | + "x_followers": null, | |
| 2595 | + "twitch_followers": null | |
| 2596 | + }, | |
| 2597 | + "category": "mode", | |
| 2598 | + "location": "Montréal", | |
| 2599 | + "notes": "Gagnant Gala InfluenceCréation 2025 — Expression artistique. | Top 20 Modash Montréal, rang 16; surcycleur de vêtements (upcycling), #CONNULIFE; engagement 3.13%", | |
| 2600 | + "sources": [ | |
| 2601 | + "billie_gala", | |
| 2602 | + "grenier_gala", | |
| 2603 | + "modash" | |
| 2604 | + ], | |
| 2605 | + "total_followers": 144200, | |
| 2606 | + "platform_count": 1, | |
| 2607 | + "tier": "macro (100K+)" | |
| 2608 | + }, | |
| 2609 | + { | |
| 2610 | + "name": "Yann Enrich", | |
| 2611 | + "handles": { | |
| 2612 | + "instagram": "samples.fr", | |
| 2613 | + "tiktok": null, | |
| 2614 | + "youtube": null, | |
| 2615 | + "facebook": null, | |
| 2616 | + "x": null, | |
| 2617 | + "twitch": null | |
| 2618 | + }, | |
| 2619 | + "metrics": { | |
| 2620 | + "instagram_followers": 137500, | |
| 2621 | + "tiktok_followers": 5500, | |
| 2622 | + "youtube_subscribers": null, | |
| 2623 | + "facebook_followers": null, | |
| 2624 | + "x_followers": null, | |
| 2625 | + "twitch_followers": null | |
| 2626 | + }, | |
| 2627 | + "category": "musique", | |
| 2628 | + "location": "Montréal", | |
| 2629 | + "notes": "vidéos sur les samples musicaux et la techno (vinyle, DJ, micro); X entre 10k et 50k", | |
| 2630 | + "sources": [ | |
| 2631 | + "collabstr" | |
| 2632 | + ], | |
| 2633 | + "total_followers": 143000, | |
| 2634 | + "platform_count": 2, | |
| 2635 | + "tier": "macro (100K+)" | |
| 2636 | + }, | |
| 2637 | + { | |
| 2638 | + "name": "Joanie Grenier", | |
| 2639 | + "handles": { | |
| 2640 | + "instagram": "joaniegrenier69", | |
| 2641 | + "tiktok": null, | |
| 2642 | + "youtube": null, | |
| 2643 | + "facebook": null, | |
| 2644 | + "x": null, | |
| 2645 | + "twitch": null | |
| 2646 | + }, | |
| 2647 | + "metrics": { | |
| 2648 | + "instagram_followers": 142900, | |
| 2649 | + "tiktok_followers": null, | |
| 2650 | + "youtube_subscribers": null, | |
| 2651 | + "facebook_followers": null, | |
| 2652 | + "x_followers": null, | |
| 2653 | + "twitch_followers": null | |
| 2654 | + }, | |
| 2655 | + "category": "humour", | |
| 2656 | + "location": "Montréal", | |
| 2657 | + "notes": "Top 20 Modash Montréal, rang 17; animatrice de podcast, contenu bien-être/sexualité; engagement 2.6%", | |
| 2658 | + "sources": [ | |
| 2659 | + "modash" | |
| 2660 | + ], | |
| 2661 | + "total_followers": 142900, | |
| 2662 | + "platform_count": 1, | |
| 2663 | + "tier": "macro (100K+)" | |
| 2664 | + }, | |
| 2665 | + { | |
| 2666 | + "name": "Pézie Beaudin", | |
| 2667 | + "handles": { | |
| 2668 | + "instagram": "pezie.beaudin", | |
| 2669 | + "tiktok": null, | |
| 2670 | + "youtube": "pézie beaudin", | |
| 2671 | + "facebook": null, | |
| 2672 | + "x": null, | |
| 2673 | + "twitch": null | |
| 2674 | + }, | |
| 2675 | + "metrics": { | |
| 2676 | + "instagram_followers": 137000, | |
| 2677 | + "tiktok_followers": null, | |
| 2678 | + "youtube_subscribers": null, | |
| 2679 | + "facebook_followers": null, | |
| 2680 | + "x_followers": null, | |
| 2681 | + "twitch_followers": null | |
| 2682 | + }, | |
| 2683 | + "category": "lifestyle", | |
| 2684 | + "location": "Québec", | |
| 2685 | + "notes": "Design d'intérieur à approche humaine; podcast « On s'connaît pas tant que ça ». Top YouTubeurs QC 2026 (ONEWAY).", | |
| 2686 | + "sources": [ | |
| 2687 | + "oneway_youtube" | |
| 2688 | + ], | |
| 2689 | + "total_followers": 137000, | |
| 2690 | + "platform_count": 2, | |
| 2691 | + "tier": "macro (100K+)" | |
| 2692 | + }, | |
| 2693 | + { | |
| 2694 | + "name": "Tectalks", | |
| 2695 | + "handles": { | |
| 2696 | + "instagram": "tectalks.io", | |
| 2697 | + "tiktok": null, | |
| 2698 | + "youtube": null, | |
| 2699 | + "facebook": null, | |
| 2700 | + "x": null, | |
| 2701 | + "twitch": null | |
| 2702 | + }, | |
| 2703 | + "metrics": { | |
| 2704 | + "instagram_followers": 132600, | |
| 2705 | + "tiktok_followers": null, | |
| 2706 | + "youtube_subscribers": 156, | |
| 2707 | + "facebook_followers": null, | |
| 2708 | + "x_followers": null, | |
| 2709 | + "twitch_followers": null | |
| 2710 | + }, | |
| 2711 | + "category": "autre", | |
| 2712 | + "location": "Montréal", | |
| 2713 | + "notes": "compte collectif tech et éducation (IA, robotique)", | |
| 2714 | + "sources": [ | |
| 2715 | + "collabstr" | |
| 2716 | + ], | |
| 2717 | + "total_followers": 132756, | |
| 2718 | + "platform_count": 2, | |
| 2719 | + "tier": "macro (100K+)" | |
| 2720 | + }, | |
| 2721 | + { | |
| 2722 | + "name": "Wassim Abdul Khalek", | |
| 2723 | + "handles": { | |
| 2724 | + "instagram": "ak.wassim", | |
| 2725 | + "tiktok": null, | |
| 2726 | + "youtube": null, | |
| 2727 | + "facebook": null, | |
| 2728 | + "x": null, | |
| 2729 | + "twitch": null | |
| 2730 | + }, | |
| 2731 | + "metrics": { | |
| 2732 | + "instagram_followers": 82900, | |
| 2733 | + "tiktok_followers": 48800, | |
| 2734 | + "youtube_subscribers": null, | |
| 2735 | + "facebook_followers": null, | |
| 2736 | + "x_followers": null, | |
| 2737 | + "twitch_followers": null | |
| 2738 | + }, | |
| 2739 | + "category": "food", | |
| 2740 | + "location": "Montréal", | |
| 2741 | + "notes": "Histoires humaines à travers la bouffe et la culture; +9% de croissance IG en 3 mois", | |
| 2742 | + "sources": [ | |
| 2743 | + "feedspot_food", | |
| 2744 | + "lagrappe_food" | |
| 2745 | + ], | |
| 2746 | + "total_followers": 131700, | |
| 2747 | + "platform_count": 2, | |
| 2748 | + "tier": "macro (100K+)" | |
| 2749 | + }, | |
| 2750 | + { | |
| 2751 | + "name": "Alex Mentink", | |
| 2752 | + "handles": { | |
| 2753 | + "instagram": "menthink", | |
| 2754 | + "tiktok": null, | |
| 2755 | + "youtube": null, | |
| 2756 | + "facebook": null, | |
| 2757 | + "x": null, | |
| 2758 | + "twitch": null | |
| 2759 | + }, | |
| 2760 | + "metrics": { | |
| 2761 | + "instagram_followers": 126900, | |
| 2762 | + "tiktok_followers": null, | |
| 2763 | + "youtube_subscribers": null, | |
| 2764 | + "facebook_followers": null, | |
| 2765 | + "x_followers": null, | |
| 2766 | + "twitch_followers": null | |
| 2767 | + }, | |
| 2768 | + "category": "entrepreneur", | |
| 2769 | + "location": "Montréal", | |
| 2770 | + "notes": "Top 20 Modash Montréal, rang 18; « The Happy Dad », fondateur @carryu.ca et @lockup.ca; engagement 3.85%", | |
| 2771 | + "sources": [ | |
| 2772 | + "modash" | |
| 2773 | + ], | |
| 2774 | + "total_followers": 126900, | |
| 2775 | + "platform_count": 1, | |
| 2776 | + "tier": "macro (100K+)" | |
| 2777 | + }, | |
| 2778 | + { | |
| 2779 | + "name": "Antonio Park", | |
| 2780 | + "handles": { | |
| 2781 | + "instagram": "chefantoniopark", | |
| 2782 | + "tiktok": null, | |
| 2783 | + "youtube": null, | |
| 2784 | + "facebook": null, | |
| 2785 | + "x": null, | |
| 2786 | + "twitch": null | |
| 2787 | + }, | |
| 2788 | + "metrics": { | |
| 2789 | + "instagram_followers": 126400, | |
| 2790 | + "tiktok_followers": null, | |
| 2791 | + "youtube_subscribers": null, | |
| 2792 | + "facebook_followers": null, | |
| 2793 | + "x_followers": null, | |
| 2794 | + "twitch_followers": null | |
| 2795 | + }, | |
| 2796 | + "category": "food", | |
| 2797 | + "location": "Montréal", | |
| 2798 | + "notes": "Chef-propriétaire (Park, Café Bazin, Jatoba, Le Cathcart)", | |
| 2799 | + "sources": [ | |
| 2800 | + "feedspot_food" | |
| 2801 | + ], | |
| 2802 | + "total_followers": 126400, | |
| 2803 | + "platform_count": 1, | |
| 2804 | + "tier": "macro (100K+)" | |
| 2805 | + }, | |
| 2806 | + { | |
| 2807 | + "name": "Antoine Olivier Pilon", | |
| 2808 | + "handles": { | |
| 2809 | + "instagram": "antoineolivierpilon", | |
| 2810 | + "tiktok": null, | |
| 2811 | + "youtube": null, | |
| 2812 | + "facebook": null, | |
| 2813 | + "x": null, | |
| 2814 | + "twitch": null | |
| 2815 | + }, | |
| 2816 | + "metrics": { | |
| 2817 | + "instagram_followers": 125200, | |
| 2818 | + "tiktok_followers": null, | |
| 2819 | + "youtube_subscribers": null, | |
| 2820 | + "facebook_followers": null, | |
| 2821 | + "x_followers": null, | |
| 2822 | + "twitch_followers": null | |
| 2823 | + }, | |
| 2824 | + "category": "autre", | |
| 2825 | + "location": "Montréal", | |
| 2826 | + "notes": "Top 20 Modash Montréal, rang 19; acteur (Mommy de Xavier Dolan); engagement 2.22%", | |
| 2827 | + "sources": [ | |
| 2828 | + "modash" | |
| 2829 | + ], | |
| 2830 | + "total_followers": 125200, | |
| 2831 | + "platform_count": 1, | |
| 2832 | + "tier": "macro (100K+)" | |
| 2833 | + }, | |
| 2834 | + { | |
| 2835 | + "name": "Max Gourgues", | |
| 2836 | + "handles": { | |
| 2837 | + "instagram": "maxgourgues", | |
| 2838 | + "tiktok": "maxgourgues", | |
| 2839 | + "youtube": "maxgourgues7062", | |
| 2840 | + "facebook": null, | |
| 2841 | + "x": null, | |
| 2842 | + "twitch": null | |
| 2843 | + }, | |
| 2844 | + "metrics": { | |
| 2845 | + "instagram_followers": 114700, | |
| 2846 | + "tiktok_followers": 1700, | |
| 2847 | + "youtube_subscribers": 700, | |
| 2848 | + "facebook_followers": 600, | |
| 2849 | + "x_followers": null, | |
| 2850 | + "twitch_followers": null | |
| 2851 | + }, | |
| 2852 | + "category": "beauté", | |
| 2853 | + "location": "Montréal", | |
| 2854 | + "notes": "Coloriste et entrepreneur (salon fondé en 2019), ambassadeur Redken; transformations capillaires virales. | Top 20 Modash Montréal, rang 20; coloriste capillaire, propriétaire @salonmaxgourgues, ambassadeur Redken; engagement 2.35%", | |
| 2855 | + "sources": [ | |
| 2856 | + "frank", | |
| 2857 | + "modash" | |
| 2858 | + ], | |
| 2859 | + "total_followers": 117700, | |
| 2860 | + "platform_count": 4, | |
| 2861 | + "tier": "macro (100K+)" | |
| 2862 | + }, | |
| 2863 | + { | |
| 2864 | + "name": "Audrey-Lyne Ferlatte", | |
| 2865 | + "handles": { | |
| 2866 | + "instagram": "ferlattez", | |
| 2867 | + "tiktok": "ferlattez", | |
| 2868 | + "youtube": null, | |
| 2869 | + "facebook": null, | |
| 2870 | + "x": null, | |
| 2871 | + "twitch": null | |
| 2872 | + }, | |
| 2873 | + "metrics": { | |
| 2874 | + "instagram_followers": 79000, | |
| 2875 | + "tiktok_followers": 17400, | |
| 2876 | + "youtube_subscribers": null, | |
| 2877 | + "facebook_followers": 21000, | |
| 2878 | + "x_followers": null, | |
| 2879 | + "twitch_followers": null | |
| 2880 | + }, | |
| 2881 | + "category": "voyage", | |
| 2882 | + "location": "Québec", | |
| 2883 | + "notes": "randonnée, roadtrips, chalets et tourisme famille; 21K sur Facebook | Maman aventurière : voyage, randonnée et paysages du Québec.", | |
| 2884 | + "sources": [ | |
| 2885 | + "feedspot", | |
| 2886 | + "embold", | |
| 2887 | + "collabstr_tiktok" | |
| 2888 | + ], | |
| 2889 | + "total_followers": 117400, | |
| 2890 | + "platform_count": 3, | |
| 2891 | + "tier": "macro (100K+)" | |
| 2892 | + }, | |
| 2893 | + { | |
| 2894 | + "name": "Solène Lucas", | |
| 2895 | + "handles": { | |
| 2896 | + "instagram": "_solenelucas", | |
| 2897 | + "tiktok": null, | |
| 2898 | + "youtube": null, | |
| 2899 | + "facebook": null, | |
| 2900 | + "x": null, | |
| 2901 | + "twitch": null | |
| 2902 | + }, | |
| 2903 | + "metrics": { | |
| 2904 | + "instagram_followers": 116300, | |
| 2905 | + "tiktok_followers": null, | |
| 2906 | + "youtube_subscribers": null, | |
| 2907 | + "facebook_followers": null, | |
| 2908 | + "x_followers": null, | |
| 2909 | + "twitch_followers": null | |
| 2910 | + }, | |
| 2911 | + "category": "fitness", | |
| 2912 | + "location": "Québec", | |
| 2913 | + "notes": "coach fitness (@aura.trainingclub), athlète AYBL", | |
| 2914 | + "sources": [ | |
| 2915 | + "feedspot" | |
| 2916 | + ], | |
| 2917 | + "total_followers": 116300, | |
| 2918 | + "platform_count": 1, | |
| 2919 | + "tier": "macro (100K+)" | |
| 2920 | + }, | |
| 2921 | + { | |
| 2922 | + "name": "Melissa Soldera", | |
| 2923 | + "handles": { | |
| 2924 | + "instagram": "melissasoldera", | |
| 2925 | + "tiktok": null, | |
| 2926 | + "youtube": null, | |
| 2927 | + "facebook": null, | |
| 2928 | + "x": null, | |
| 2929 | + "twitch": null | |
| 2930 | + }, | |
| 2931 | + "metrics": { | |
| 2932 | + "instagram_followers": 112200, | |
| 2933 | + "tiktok_followers": null, | |
| 2934 | + "youtube_subscribers": null, | |
| 2935 | + "facebook_followers": null, | |
| 2936 | + "x_followers": null, | |
| 2937 | + "twitch_followers": null | |
| 2938 | + }, | |
| 2939 | + "category": "lifestyle", | |
| 2940 | + "location": "Montréal", | |
| 2941 | + "notes": "maman montréalaise, food/mode/voyage, marketing numérique", | |
| 2942 | + "sources": [ | |
| 2943 | + "feedspot" | |
| 2944 | + ], | |
| 2945 | + "total_followers": 112200, | |
| 2946 | + "platform_count": 1, | |
| 2947 | + "tier": "macro (100K+)" | |
| 2948 | + }, | |
| 2949 | + { | |
| 2950 | + "name": "Helli Patel", | |
| 2951 | + "handles": { | |
| 2952 | + "instagram": null, | |
| 2953 | + "tiktok": "hellipatell", | |
| 2954 | + "youtube": null, | |
| 2955 | + "facebook": null, | |
| 2956 | + "x": null, | |
| 2957 | + "twitch": null | |
| 2958 | + }, | |
| 2959 | + "metrics": { | |
| 2960 | + "instagram_followers": 25400, | |
| 2961 | + "tiktok_followers": 82200, | |
| 2962 | + "youtube_subscribers": null, | |
| 2963 | + "facebook_followers": null, | |
| 2964 | + "x_followers": null, | |
| 2965 | + "twitch_followers": null | |
| 2966 | + }, | |
| 2967 | + "category": "fitness", | |
| 2968 | + "location": "Québec", | |
| 2969 | + "notes": "Lifestyle, fitness et bien-être. YouTube 50k-100k.", | |
| 2970 | + "sources": [ | |
| 2971 | + "collabstr_tiktok" | |
| 2972 | + ], | |
| 2973 | + "total_followers": 107600, | |
| 2974 | + "platform_count": 2, | |
| 2975 | + "tier": "macro (100K+)" | |
| 2976 | + }, | |
| 2977 | + { | |
| 2978 | + "name": "Claude Alicia", | |
| 2979 | + "handles": { | |
| 2980 | + "instagram": "consciencecoupable", | |
| 2981 | + "tiktok": null, | |
| 2982 | + "youtube": null, | |
| 2983 | + "facebook": null, | |
| 2984 | + "x": null, | |
| 2985 | + "twitch": null | |
| 2986 | + }, | |
| 2987 | + "metrics": { | |
| 2988 | + "instagram_followers": 103200, | |
| 2989 | + "tiktok_followers": null, | |
| 2990 | + "youtube_subscribers": null, | |
| 2991 | + "facebook_followers": null, | |
| 2992 | + "x_followers": null, | |
| 2993 | + "twitch_followers": null | |
| 2994 | + }, | |
| 2995 | + "category": "lifestyle", | |
| 2996 | + "location": "Montréal", | |
| 2997 | + "notes": "bébés, sacs, Disney World", | |
| 2998 | + "sources": [ | |
| 2999 | + "feedspot" | |
| 3000 | + ], | |
| 3001 | + "total_followers": 103200, | |
| 3002 | + "platform_count": 1, | |
| 3003 | + "tier": "macro (100K+)" | |
| 3004 | + }, | |
| 3005 | + { | |
| 3006 | + "name": "Erica Syl", | |
| 3007 | + "handles": { | |
| 3008 | + "instagram": "so.sylly", | |
| 3009 | + "tiktok": null, | |
| 3010 | + "youtube": null, | |
| 3011 | + "facebook": null, | |
| 3012 | + "x": null, | |
| 3013 | + "twitch": null | |
| 3014 | + }, | |
| 3015 | + "metrics": { | |
| 3016 | + "instagram_followers": 103000, | |
| 3017 | + "tiktok_followers": null, | |
| 3018 | + "youtube_subscribers": null, | |
| 3019 | + "facebook_followers": null, | |
| 3020 | + "x_followers": null, | |
| 3021 | + "twitch_followers": null | |
| 3022 | + }, | |
| 3023 | + "category": "mode", | |
| 3024 | + "location": "Montréal", | |
| 3025 | + "notes": "mode DIY durable; compte secondaire @erica.syl (103K sur l'embed)", | |
| 3026 | + "sources": [ | |
| 3027 | + "embold" | |
| 3028 | + ], | |
| 3029 | + "total_followers": 103000, | |
| 3030 | + "platform_count": 1, | |
| 3031 | + "tier": "macro (100K+)" | |
| 3032 | + }, | |
| 3033 | + { | |
| 3034 | + "name": "Bonne Bouffe Montréal", | |
| 3035 | + "handles": { | |
| 3036 | + "instagram": "bonnebouffemontreal", | |
| 3037 | + "tiktok": null, | |
| 3038 | + "youtube": null, | |
| 3039 | + "facebook": null, | |
| 3040 | + "x": null, | |
| 3041 | + "twitch": null | |
| 3042 | + }, | |
| 3043 | + "metrics": { | |
| 3044 | + "instagram_followers": 47600, | |
| 3045 | + "tiktok_followers": 55200, | |
| 3046 | + "youtube_subscribers": null, | |
| 3047 | + "facebook_followers": null, | |
| 3048 | + "x_followers": null, | |
| 3049 | + "twitch_followers": null | |
| 3050 | + }, | |
| 3051 | + "category": "food", | |
| 3052 | + "location": "Montréal", | |
| 3053 | + "notes": "Compte de recommandations de restaurants montréalais; top 7 food La Grappe 2026", | |
| 3054 | + "sources": [ | |
| 3055 | + "lagrappe_food" | |
| 3056 | + ], | |
| 3057 | + "total_followers": 102800, | |
| 3058 | + "platform_count": 2, | |
| 3059 | + "tier": "macro (100K+)" | |
| 3060 | + }, | |
| 3061 | + { | |
| 3062 | + "name": "Victoria Rodriguez Carnevale", | |
| 3063 | + "handles": { | |
| 3064 | + "instagram": "vic_carnevale", | |
| 3065 | + "tiktok": null, | |
| 3066 | + "youtube": null, | |
| 3067 | + "facebook": null, | |
| 3068 | + "x": null, | |
| 3069 | + "twitch": null | |
| 3070 | + }, | |
| 3071 | + "metrics": { | |
| 3072 | + "instagram_followers": 98300, | |
| 3073 | + "tiktok_followers": null, | |
| 3074 | + "youtube_subscribers": null, | |
| 3075 | + "facebook_followers": null, | |
| 3076 | + "x_followers": null, | |
| 3077 | + "twitch_followers": null | |
| 3078 | + }, | |
| 3079 | + "category": "mode", | |
| 3080 | + "location": "Montréal", | |
| 3081 | + "notes": "créatrice mode grande taille (plus size), beauté et lifestyle", | |
| 3082 | + "sources": [ | |
| 3083 | + "feedspot", | |
| 3084 | + "embold" | |
| 3085 | + ], | |
| 3086 | + "total_followers": 98300, | |
| 3087 | + "platform_count": 1, | |
| 3088 | + "tier": "micro (10K+)" | |
| 3089 | + }, | |
| 3090 | + { | |
| 3091 | + "name": "Bob Le Chef (Robert James Penny)", | |
| 3092 | + "handles": { | |
| 3093 | + "instagram": "boblechef", | |
| 3094 | + "tiktok": null, | |
| 3095 | + "youtube": "boblechef", | |
| 3096 | + "facebook": null, | |
| 3097 | + "x": null, | |
| 3098 | + "twitch": null | |
| 3099 | + }, | |
| 3100 | + "metrics": { | |
| 3101 | + "instagram_followers": 94600, | |
| 3102 | + "tiktok_followers": null, | |
| 3103 | + "youtube_subscribers": null, | |
| 3104 | + "facebook_followers": null, | |
| 3105 | + "x_followers": null, | |
| 3106 | + "twitch_followers": null | |
| 3107 | + }, | |
| 3108 | + "category": "food", | |
| 3109 | + "location": "Québec", | |
| 3110 | + "notes": "L'Anarchie Culinaire depuis 2005; auteur de 3 livres, lives du dimanche; restaurant Normandin", | |
| 3111 | + "sources": [ | |
| 3112 | + "feedspot_food", | |
| 3113 | + "clark_food" | |
| 3114 | + ], | |
| 3115 | + "total_followers": 94600, | |
| 3116 | + "platform_count": 2, | |
| 3117 | + "tier": "micro (10K+)" | |
| 3118 | + }, | |
| 3119 | + { | |
| 3120 | + "name": "Noura Agrebi", | |
| 3121 | + "handles": { | |
| 3122 | + "instagram": null, | |
| 3123 | + "tiktok": "noura_agrebi", | |
| 3124 | + "youtube": null, | |
| 3125 | + "facebook": null, | |
| 3126 | + "x": null, | |
| 3127 | + "twitch": null | |
| 3128 | + }, | |
| 3129 | + "metrics": { | |
| 3130 | + "instagram_followers": null, | |
| 3131 | + "tiktok_followers": 90600, | |
| 3132 | + "youtube_subscribers": null, | |
| 3133 | + "facebook_followers": null, | |
| 3134 | + "x_followers": null, | |
| 3135 | + "twitch_followers": null | |
| 3136 | + }, | |
| 3137 | + "category": "beauté", | |
| 3138 | + "location": "Québec", | |
| 3139 | + "notes": "Créatrice basée au Québec : skincare, beauté et lifestyle, UGC.", | |
| 3140 | + "sources": [ | |
| 3141 | + "collabstr_tiktok" | |
| 3142 | + ], | |
| 3143 | + "total_followers": 90600, | |
| 3144 | + "platform_count": 1, | |
| 3145 | + "tier": "micro (10K+)" | |
| 3146 | + }, | |
| 3147 | + { | |
| 3148 | + "name": "Stéphanie Bureau", | |
| 3149 | + "handles": { | |
| 3150 | + "instagram": "stephaniebureau", | |
| 3151 | + "tiktok": null, | |
| 3152 | + "youtube": null, | |
| 3153 | + "facebook": null, | |
| 3154 | + "x": null, | |
| 3155 | + "twitch": null | |
| 3156 | + }, | |
| 3157 | + "metrics": { | |
| 3158 | + "instagram_followers": 89000, | |
| 3159 | + "tiktok_followers": null, | |
| 3160 | + "youtube_subscribers": null, | |
| 3161 | + "facebook_followers": null, | |
| 3162 | + "x_followers": null, | |
| 3163 | + "twitch_followers": null | |
| 3164 | + }, | |
| 3165 | + "category": "voyage", | |
| 3166 | + "location": "Québec", | |
| 3167 | + "notes": "randonnée et inspiration voyage, croissance +16% (3 mois)", | |
| 3168 | + "sources": [ | |
| 3169 | + "feedspot" | |
| 3170 | + ], | |
| 3171 | + "total_followers": 89000, | |
| 3172 | + "platform_count": 1, | |
| 3173 | + "tier": "micro (10K+)" | |
| 3174 | + }, | |
| 3175 | + { | |
| 3176 | + "name": "Alexane Pelletier", | |
| 3177 | + "handles": { | |
| 3178 | + "instagram": "alexane_pelletier", | |
| 3179 | + "tiktok": null, | |
| 3180 | + "youtube": "alexane pelletier", | |
| 3181 | + "facebook": null, | |
| 3182 | + "x": null, | |
| 3183 | + "twitch": null | |
| 3184 | + }, | |
| 3185 | + "metrics": { | |
| 3186 | + "instagram_followers": 87000, | |
| 3187 | + "tiktok_followers": null, | |
| 3188 | + "youtube_subscribers": null, | |
| 3189 | + "facebook_followers": null, | |
| 3190 | + "x_followers": null, | |
| 3191 | + "twitch_followers": null | |
| 3192 | + }, | |
| 3193 | + "category": "lifestyle", | |
| 3194 | + "location": "Québec", | |
| 3195 | + "notes": "Créatrice autodidacte (idée au montage); connue pour son Vlogmas (24 vidéos en 24 jours). Top YouTubeurs QC 2026 (ONEWAY).", | |
| 3196 | + "sources": [ | |
| 3197 | + "oneway_youtube" | |
| 3198 | + ], | |
| 3199 | + "total_followers": 87000, | |
| 3200 | + "platform_count": 2, | |
| 3201 | + "tier": "micro (10K+)" | |
| 3202 | + }, | |
| 3203 | + { | |
| 3204 | + "name": "Sarah (Une famille au Canada)", | |
| 3205 | + "handles": { | |
| 3206 | + "instagram": "unefamilleaucanada", | |
| 3207 | + "tiktok": null, | |
| 3208 | + "youtube": null, | |
| 3209 | + "facebook": null, | |
| 3210 | + "x": null, | |
| 3211 | + "twitch": null | |
| 3212 | + }, | |
| 3213 | + "metrics": { | |
| 3214 | + "instagram_followers": 79000, | |
| 3215 | + "tiktok_followers": null, | |
| 3216 | + "youtube_subscribers": 2600, | |
| 3217 | + "facebook_followers": null, | |
| 3218 | + "x_followers": null, | |
| 3219 | + "twitch_followers": null | |
| 3220 | + }, | |
| 3221 | + "category": "voyage", | |
| 3222 | + "location": "Montréal", | |
| 3223 | + "notes": "famille française expatriée à Montréal, voyage et randonnée", | |
| 3224 | + "sources": [ | |
| 3225 | + "collabstr" | |
| 3226 | + ], | |
| 3227 | + "total_followers": 81600, | |
| 3228 | + "platform_count": 2, | |
| 3229 | + "tier": "micro (10K+)" | |
| 3230 | + }, | |
| 3231 | + { | |
| 3232 | + "name": "Alexia Colantuono", | |
| 3233 | + "handles": { | |
| 3234 | + "instagram": "alecolantuono", | |
| 3235 | + "tiktok": null, | |
| 3236 | + "youtube": null, | |
| 3237 | + "facebook": null, | |
| 3238 | + "x": null, | |
| 3239 | + "twitch": null | |
| 3240 | + }, | |
| 3241 | + "metrics": { | |
| 3242 | + "instagram_followers": 79100, | |
| 3243 | + "tiktok_followers": null, | |
| 3244 | + "youtube_subscribers": null, | |
| 3245 | + "facebook_followers": null, | |
| 3246 | + "x_followers": null, | |
| 3247 | + "twitch_followers": null | |
| 3248 | + }, | |
| 3249 | + "category": "lifestyle", | |
| 3250 | + "location": "Montréal", | |
| 3251 | + "notes": "routines bien-être et vie de maman", | |
| 3252 | + "sources": [ | |
| 3253 | + "feedspot" | |
| 3254 | + ], | |
| 3255 | + "total_followers": 79100, | |
| 3256 | + "platform_count": 1, | |
| 3257 | + "tier": "micro (10K+)" | |
| 3258 | + }, | |
| 3259 | + { | |
| 3260 | + "name": "Lydiane St-Onge (Lydiane autour du monde)", | |
| 3261 | + "handles": { | |
| 3262 | + "instagram": "lydianeautourdumonde", | |
| 3263 | + "tiktok": null, | |
| 3264 | + "youtube": null, | |
| 3265 | + "facebook": null, | |
| 3266 | + "x": null, | |
| 3267 | + "twitch": null | |
| 3268 | + }, | |
| 3269 | + "metrics": { | |
| 3270 | + "instagram_followers": 78900, | |
| 3271 | + "tiktok_followers": null, | |
| 3272 | + "youtube_subscribers": null, | |
| 3273 | + "facebook_followers": null, | |
| 3274 | + "x_followers": null, | |
| 3275 | + "twitch_followers": null | |
| 3276 | + }, | |
| 3277 | + "category": "voyage", | |
| 3278 | + "location": "Québec", | |
| 3279 | + "notes": "Blogueuse voyage incontournable, passe une grande partie de l'année à l'étranger", | |
| 3280 | + "sources": [ | |
| 3281 | + "noovomoi_voyage" | |
| 3282 | + ], | |
| 3283 | + "total_followers": 78900, | |
| 3284 | + "platform_count": 1, | |
| 3285 | + "tier": "micro (10K+)" | |
| 3286 | + }, | |
| 3287 | + { | |
| 3288 | + "name": "Emma Bossé", | |
| 3289 | + "handles": { | |
| 3290 | + "instagram": "emma04044", | |
| 3291 | + "tiktok": null, | |
| 3292 | + "youtube": "emma04044", | |
| 3293 | + "facebook": null, | |
| 3294 | + "x": null, | |
| 3295 | + "twitch": null | |
| 3296 | + }, | |
| 3297 | + "metrics": { | |
| 3298 | + "instagram_followers": 74700, | |
| 3299 | + "tiktok_followers": null, | |
| 3300 | + "youtube_subscribers": null, | |
| 3301 | + "facebook_followers": null, | |
| 3302 | + "x_followers": null, | |
| 3303 | + "twitch_followers": null | |
| 3304 | + }, | |
| 3305 | + "category": "autre", | |
| 3306 | + "location": "Québec", | |
| 3307 | + "notes": "Chaîne axée astrologie et tarot; nombre d'abonnés gardé secret en 2022 (~300 000 en 2021).", | |
| 3308 | + "sources": [ | |
| 3309 | + "billie_youtube" | |
| 3310 | + ], | |
| 3311 | + "total_followers": 74700, | |
| 3312 | + "platform_count": 2, | |
| 3313 | + "tier": "micro (10K+)" | |
| 3314 | + }, | |
| 3315 | + { | |
| 3316 | + "name": "Cassandra Loignon", | |
| 3317 | + "handles": { | |
| 3318 | + "instagram": "cassandraloignon", | |
| 3319 | + "tiktok": null, | |
| 3320 | + "youtube": null, | |
| 3321 | + "facebook": null, | |
| 3322 | + "x": null, | |
| 3323 | + "twitch": null | |
| 3324 | + }, | |
| 3325 | + "metrics": { | |
| 3326 | + "instagram_followers": 73000, | |
| 3327 | + "tiktok_followers": null, | |
| 3328 | + "youtube_subscribers": null, | |
| 3329 | + "facebook_followers": null, | |
| 3330 | + "x_followers": null, | |
| 3331 | + "twitch_followers": null | |
| 3332 | + }, | |
| 3333 | + "category": "food", | |
| 3334 | + "location": "Québec", | |
| 3335 | + "notes": "Entrepreneure (institut de beauté Les Précieuses); recettes quotidiennes en stories; chiffres de 2019", | |
| 3336 | + "sources": [ | |
| 3337 | + "clark_food" | |
| 3338 | + ], | |
| 3339 | + "total_followers": 73000, | |
| 3340 | + "platform_count": 1, | |
| 3341 | + "tier": "micro (10K+)" | |
| 3342 | + }, | |
| 3343 | + { | |
| 3344 | + "name": "Maxime Tremblay (Max L'Affamé)", | |
| 3345 | + "handles": { | |
| 3346 | + "instagram": "max_laffame", | |
| 3347 | + "tiktok": null, | |
| 3348 | + "youtube": null, | |
| 3349 | + "facebook": null, | |
| 3350 | + "x": null, | |
| 3351 | + "twitch": null | |
| 3352 | + }, | |
| 3353 | + "metrics": { | |
| 3354 | + "instagram_followers": 70400, | |
| 3355 | + "tiktok_followers": null, | |
| 3356 | + "youtube_subscribers": null, | |
| 3357 | + "facebook_followers": null, | |
| 3358 | + "x_followers": null, | |
| 3359 | + "twitch_followers": null | |
| 3360 | + }, | |
| 3361 | + "category": "food", | |
| 3362 | + "location": "Montréal", | |
| 3363 | + "notes": "Livre et série « AirFryer tout-en-un », série sur NoovoMoi.ca", | |
| 3364 | + "sources": [ | |
| 3365 | + "feedspot_food" | |
| 3366 | + ], | |
| 3367 | + "total_followers": 70400, | |
| 3368 | + "platform_count": 1, | |
| 3369 | + "tier": "micro (10K+)" | |
| 3370 | + }, | |
| 3371 | + { | |
| 3372 | + "name": "Logan Medeiros", | |
| 3373 | + "handles": { | |
| 3374 | + "instagram": "loganmedeiros", | |
| 3375 | + "tiktok": null, | |
| 3376 | + "youtube": null, | |
| 3377 | + "facebook": null, | |
| 3378 | + "x": null, | |
| 3379 | + "twitch": null | |
| 3380 | + }, | |
| 3381 | + "metrics": { | |
| 3382 | + "instagram_followers": 69300, | |
| 3383 | + "tiktok_followers": null, | |
| 3384 | + "youtube_subscribers": null, | |
| 3385 | + "facebook_followers": null, | |
| 3386 | + "x_followers": null, | |
| 3387 | + "twitch_followers": null | |
| 3388 | + }, | |
| 3389 | + "category": "lifestyle", | |
| 3390 | + "location": "Montréal", | |
| 3391 | + "notes": "lifestyle, bien-être, beauté et mode; croissance +32% (3 mois)", | |
| 3392 | + "sources": [ | |
| 3393 | + "feedspot" | |
| 3394 | + ], | |
| 3395 | + "total_followers": 69300, | |
| 3396 | + "platform_count": 1, | |
| 3397 | + "tier": "micro (10K+)" | |
| 3398 | + }, | |
| 3399 | + { | |
| 3400 | + "name": "AchilleFPS", | |
| 3401 | + "handles": { | |
| 3402 | + "instagram": "achillefps", | |
| 3403 | + "tiktok": null, | |
| 3404 | + "youtube": null, | |
| 3405 | + "facebook": null, | |
| 3406 | + "x": null, | |
| 3407 | + "twitch": "achillefps" | |
| 3408 | + }, | |
| 3409 | + "metrics": { | |
| 3410 | + "instagram_followers": 69200, | |
| 3411 | + "tiktok_followers": null, | |
| 3412 | + "youtube_subscribers": null, | |
| 3413 | + "facebook_followers": null, | |
| 3414 | + "x_followers": null, | |
| 3415 | + "twitch_followers": null | |
| 3416 | + }, | |
| 3417 | + "category": "gaming", | |
| 3418 | + "location": "Québec", | |
| 3419 | + "notes": "Streamer FPS/Battle Royale depuis 2016 (H1Z1, PUBG, Apex, Fortnite, Warzone)", | |
| 3420 | + "sources": [ | |
| 3421 | + "leregard_twitch" | |
| 3422 | + ], | |
| 3423 | + "total_followers": 69200, | |
| 3424 | + "platform_count": 2, | |
| 3425 | + "tier": "micro (10K+)" | |
| 3426 | + }, | |
| 3427 | + { | |
| 3428 | + "name": "Prêts pour la route", | |
| 3429 | + "handles": { | |
| 3430 | + "instagram": null, | |
| 3431 | + "tiktok": null, | |
| 3432 | + "youtube": "prêts pour la route", | |
| 3433 | + "facebook": null, | |
| 3434 | + "x": null, | |
| 3435 | + "twitch": null | |
| 3436 | + }, | |
| 3437 | + "metrics": { | |
| 3438 | + "instagram_followers": null, | |
| 3439 | + "tiktok_followers": null, | |
| 3440 | + "youtube_subscribers": 69100, | |
| 3441 | + "facebook_followers": null, | |
| 3442 | + "x_followers": null, | |
| 3443 | + "twitch_followers": null | |
| 3444 | + }, | |
| 3445 | + "category": "voyage", | |
| 3446 | + "location": "Québec", | |
| 3447 | + "notes": "Couple qui vit en roulotte à l'année et filme ses aventures (vanlife); #5 du top voyage VoyageurQc", | |
| 3448 | + "sources": [ | |
| 3449 | + "voyageurqc" | |
| 3450 | + ], | |
| 3451 | + "total_followers": 69100, | |
| 3452 | + "platform_count": 1, | |
| 3453 | + "tier": "micro (10K+)" | |
| 3454 | + }, | |
| 3455 | + { | |
| 3456 | + "name": "Maya (MTL Foodie)", | |
| 3457 | + "handles": { | |
| 3458 | + "instagram": "mtl.foodie", | |
| 3459 | + "tiktok": null, | |
| 3460 | + "youtube": null, | |
| 3461 | + "facebook": null, | |
| 3462 | + "x": null, | |
| 3463 | + "twitch": null | |
| 3464 | + }, | |
| 3465 | + "metrics": { | |
| 3466 | + "instagram_followers": 59700, | |
| 3467 | + "tiktok_followers": null, | |
| 3468 | + "youtube_subscribers": null, | |
| 3469 | + "facebook_followers": null, | |
| 3470 | + "x_followers": null, | |
| 3471 | + "twitch_followers": null | |
| 3472 | + }, | |
| 3473 | + "category": "food", | |
| 3474 | + "location": "Montréal", | |
| 3475 | + "notes": "Créatrice food montréalaise", | |
| 3476 | + "sources": [ | |
| 3477 | + "feedspot_food" | |
| 3478 | + ], | |
| 3479 | + "total_followers": 59700, | |
| 3480 | + "platform_count": 1, | |
| 3481 | + "tier": "micro (10K+)" | |
| 3482 | + }, | |
| 3483 | + { | |
| 3484 | + "name": "Ariane Toussaint", | |
| 3485 | + "handles": { | |
| 3486 | + "instagram": "arianetouss", | |
| 3487 | + "tiktok": null, | |
| 3488 | + "youtube": null, | |
| 3489 | + "facebook": null, | |
| 3490 | + "x": null, | |
| 3491 | + "twitch": null | |
| 3492 | + }, | |
| 3493 | + "metrics": { | |
| 3494 | + "instagram_followers": 57400, | |
| 3495 | + "tiktok_followers": null, | |
| 3496 | + "youtube_subscribers": null, | |
| 3497 | + "facebook_followers": null, | |
| 3498 | + "x_followers": null, | |
| 3499 | + "twitch_followers": null | |
| 3500 | + }, | |
| 3501 | + "category": "lifestyle", | |
| 3502 | + "location": "Québec", | |
| 3503 | + "notes": "lifestyle western, infirmière clinique, ambassadrice @missrodeoqc", | |
| 3504 | + "sources": [ | |
| 3505 | + "feedspot" | |
| 3506 | + ], | |
| 3507 | + "total_followers": 57400, | |
| 3508 | + "platform_count": 1, | |
| 3509 | + "tier": "micro (10K+)" | |
| 3510 | + }, | |
| 3511 | + { | |
| 3512 | + "name": "Latifah Yene (Badgalatii)", | |
| 3513 | + "handles": { | |
| 3514 | + "instagram": null, | |
| 3515 | + "tiktok": "misslatii", | |
| 3516 | + "youtube": null, | |
| 3517 | + "facebook": null, | |
| 3518 | + "x": null, | |
| 3519 | + "twitch": null | |
| 3520 | + }, | |
| 3521 | + "metrics": { | |
| 3522 | + "instagram_followers": 13200, | |
| 3523 | + "tiktok_followers": 43200, | |
| 3524 | + "youtube_subscribers": null, | |
| 3525 | + "facebook_followers": null, | |
| 3526 | + "x_followers": null, | |
| 3527 | + "twitch_followers": null | |
| 3528 | + }, | |
| 3529 | + "category": "mode", | |
| 3530 | + "location": "Québec", | |
| 3531 | + "notes": "Mode, beauté et lifestyle avec une esthétique féminine affirmée.", | |
| 3532 | + "sources": [ | |
| 3533 | + "collabstr_tiktok" | |
| 3534 | + ], | |
| 3535 | + "total_followers": 56400, | |
| 3536 | + "platform_count": 2, | |
| 3537 | + "tier": "micro (10K+)" | |
| 3538 | + }, | |
| 3539 | + { | |
| 3540 | + "name": "Narissara Dangjean", | |
| 3541 | + "handles": { | |
| 3542 | + "instagram": null, | |
| 3543 | + "tiktok": "makimild01", | |
| 3544 | + "youtube": null, | |
| 3545 | + "facebook": null, | |
| 3546 | + "x": null, | |
| 3547 | + "twitch": null | |
| 3548 | + }, | |
| 3549 | + "metrics": { | |
| 3550 | + "instagram_followers": null, | |
| 3551 | + "tiktok_followers": 55700, | |
| 3552 | + "youtube_subscribers": null, | |
| 3553 | + "facebook_followers": null, | |
| 3554 | + "x_followers": null, | |
| 3555 | + "twitch_followers": null | |
| 3556 | + }, | |
| 3557 | + "category": "beauté", | |
| 3558 | + "location": "Québec", | |
| 3559 | + "notes": "Skincare, maquillage, bien-être, fitness et plein air.", | |
| 3560 | + "sources": [ | |
| 3561 | + "collabstr_tiktok" | |
| 3562 | + ], | |
| 3563 | + "total_followers": 55700, | |
| 3564 | + "platform_count": 1, | |
| 3565 | + "tier": "micro (10K+)" | |
| 3566 | + }, | |
| 3567 | + { | |
| 3568 | + "name": "Mica Peng", | |
| 3569 | + "handles": { | |
| 3570 | + "instagram": "micapeng", | |
| 3571 | + "tiktok": null, | |
| 3572 | + "youtube": null, | |
| 3573 | + "facebook": null, | |
| 3574 | + "x": null, | |
| 3575 | + "twitch": null | |
| 3576 | + }, | |
| 3577 | + "metrics": { | |
| 3578 | + "instagram_followers": 55300, | |
| 3579 | + "tiktok_followers": null, | |
| 3580 | + "youtube_subscribers": null, | |
| 3581 | + "facebook_followers": null, | |
| 3582 | + "x_followers": null, | |
| 3583 | + "twitch_followers": null | |
| 3584 | + }, | |
| 3585 | + "category": "beauté", | |
| 3586 | + "location": "Montréal", | |
| 3587 | + "notes": "beauté, bien-être et storytelling", | |
| 3588 | + "sources": [ | |
| 3589 | + "feedspot" | |
| 3590 | + ], | |
| 3591 | + "total_followers": 55300, | |
| 3592 | + "platform_count": 1, | |
| 3593 | + "tier": "micro (10K+)" | |
| 3594 | + }, | |
| 3595 | + { | |
| 3596 | + "name": "Arnaud Nadeau", | |
| 3597 | + "handles": { | |
| 3598 | + "instagram": "arnaud.od_officiel", | |
| 3599 | + "tiktok": null, | |
| 3600 | + "youtube": null, | |
| 3601 | + "facebook": null, | |
| 3602 | + "x": null, | |
| 3603 | + "twitch": null | |
| 3604 | + }, | |
| 3605 | + "metrics": { | |
| 3606 | + "instagram_followers": 55300, | |
| 3607 | + "tiktok_followers": null, | |
| 3608 | + "youtube_subscribers": null, | |
| 3609 | + "facebook_followers": null, | |
| 3610 | + "x_followers": null, | |
| 3611 | + "twitch_followers": null | |
| 3612 | + }, | |
| 3613 | + "category": "télé-réalité", | |
| 3614 | + "location": "Montréal", | |
| 3615 | + "notes": "Entrepreneur montréalais, gagnant d'Occupation Double Chypre 2025, a lancé sa chaîne Twitch", | |
| 3616 | + "sources": [ | |
| 3617 | + "leregard_twitch" | |
| 3618 | + ], | |
| 3619 | + "total_followers": 55300, | |
| 3620 | + "platform_count": 1, | |
| 3621 | + "tier": "micro (10K+)" | |
| 3622 | + }, | |
| 3623 | + { | |
| 3624 | + "name": "Papi Melv", | |
| 3625 | + "handles": { | |
| 3626 | + "instagram": "papimelv", | |
| 3627 | + "tiktok": null, | |
| 3628 | + "youtube": null, | |
| 3629 | + "facebook": null, | |
| 3630 | + "x": null, | |
| 3631 | + "twitch": "papimelv" | |
| 3632 | + }, | |
| 3633 | + "metrics": { | |
| 3634 | + "instagram_followers": 53000, | |
| 3635 | + "tiktok_followers": null, | |
| 3636 | + "youtube_subscribers": null, | |
| 3637 | + "facebook_followers": null, | |
| 3638 | + "x_followers": null, | |
| 3639 | + "twitch_followers": null | |
| 3640 | + }, | |
| 3641 | + "category": "gaming", | |
| 3642 | + "location": "Québec", | |
| 3643 | + "notes": "Streaming IRL, lives de voyages à l'étranger (Inde, Italie)", | |
| 3644 | + "sources": [ | |
| 3645 | + "leregard_twitch" | |
| 3646 | + ], | |
| 3647 | + "total_followers": 53000, | |
| 3648 | + "platform_count": 2, | |
| 3649 | + "tier": "micro (10K+)" | |
| 3650 | + }, | |
| 3651 | + { | |
| 3652 | + "name": "Zainab (zai.eats)", | |
| 3653 | + "handles": { | |
| 3654 | + "instagram": "zai.eats", | |
| 3655 | + "tiktok": null, | |
| 3656 | + "youtube": null, | |
| 3657 | + "facebook": null, | |
| 3658 | + "x": null, | |
| 3659 | + "twitch": null | |
| 3660 | + }, | |
| 3661 | + "metrics": { | |
| 3662 | + "instagram_followers": 51900, | |
| 3663 | + "tiktok_followers": null, | |
| 3664 | + "youtube_subscribers": null, | |
| 3665 | + "facebook_followers": null, | |
| 3666 | + "x_followers": null, | |
| 3667 | + "twitch_followers": null | |
| 3668 | + }, | |
| 3669 | + "category": "food", | |
| 3670 | + "location": "Montréal", | |
| 3671 | + "notes": "Quoi manger et où sortir à Montréal; vlogs de voyage", | |
| 3672 | + "sources": [ | |
| 3673 | + "feedspot_food" | |
| 3674 | + ], | |
| 3675 | + "total_followers": 51900, | |
| 3676 | + "platform_count": 1, | |
| 3677 | + "tier": "micro (10K+)" | |
| 3678 | + }, | |
| 3679 | + { | |
| 3680 | + "name": "Manvi Gangwani", | |
| 3681 | + "handles": { | |
| 3682 | + "instagram": "yourbossgirll", | |
| 3683 | + "tiktok": null, | |
| 3684 | + "youtube": null, | |
| 3685 | + "facebook": null, | |
| 3686 | + "x": null, | |
| 3687 | + "twitch": null | |
| 3688 | + }, | |
| 3689 | + "metrics": { | |
| 3690 | + "instagram_followers": 28600, | |
| 3691 | + "tiktok_followers": null, | |
| 3692 | + "youtube_subscribers": 23000, | |
| 3693 | + "facebook_followers": null, | |
| 3694 | + "x_followers": null, | |
| 3695 | + "twitch_followers": null | |
| 3696 | + }, | |
| 3697 | + "category": "autre", | |
| 3698 | + "location": "Montréal", | |
| 3699 | + "notes": "artiste muraliste (@bossgirldecors), contenu desi au Canada, 23K sur YouTube", | |
| 3700 | + "sources": [ | |
| 3701 | + "feedspot" | |
| 3702 | + ], | |
| 3703 | + "total_followers": 51600, | |
| 3704 | + "platform_count": 2, | |
| 3705 | + "tier": "micro (10K+)" | |
| 3706 | + }, | |
| 3707 | + { | |
| 3708 | + "name": "Joe Beef", | |
| 3709 | + "handles": { | |
| 3710 | + "instagram": "joebeef", | |
| 3711 | + "tiktok": null, | |
| 3712 | + "youtube": null, | |
| 3713 | + "facebook": null, | |
| 3714 | + "x": null, | |
| 3715 | + "twitch": null | |
| 3716 | + }, | |
| 3717 | + "metrics": { | |
| 3718 | + "instagram_followers": 46100, | |
| 3719 | + "tiktok_followers": null, | |
| 3720 | + "youtube_subscribers": null, | |
| 3721 | + "facebook_followers": null, | |
| 3722 | + "x_followers": null, | |
| 3723 | + "twitch_followers": null | |
| 3724 | + }, | |
| 3725 | + "category": "food", | |
| 3726 | + "location": "Montréal", | |
| 3727 | + "notes": "Compte du restaurant emblématique montréalais Joe Beef", | |
| 3728 | + "sources": [ | |
| 3729 | + "feedspot_food" | |
| 3730 | + ], | |
| 3731 | + "total_followers": 46100, | |
| 3732 | + "platform_count": 1, | |
| 3733 | + "tier": "micro (10K+)" | |
| 3734 | + }, | |
| 3735 | + { | |
| 3736 | + "name": "Shaielle Oliveira", | |
| 3737 | + "handles": { | |
| 3738 | + "instagram": null, | |
| 3739 | + "tiktok": "queenz_shaii", | |
| 3740 | + "youtube": null, | |
| 3741 | + "facebook": null, | |
| 3742 | + "x": null, | |
| 3743 | + "twitch": null | |
| 3744 | + }, | |
| 3745 | + "metrics": { | |
| 3746 | + "instagram_followers": 34500, | |
| 3747 | + "tiktok_followers": 8600, | |
| 3748 | + "youtube_subscribers": null, | |
| 3749 | + "facebook_followers": null, | |
| 3750 | + "x_followers": null, | |
| 3751 | + "twitch_followers": null | |
| 3752 | + }, | |
| 3753 | + "category": "beauté", | |
| 3754 | + "location": "Québec", | |
| 3755 | + "notes": "Maman créatrice UGC : skincare, beauté, mode, DIY.", | |
| 3756 | + "sources": [ | |
| 3757 | + "collabstr_tiktok" | |
| 3758 | + ], | |
| 3759 | + "total_followers": 43100, | |
| 3760 | + "platform_count": 2, | |
| 3761 | + "tier": "micro (10K+)" | |
| 3762 | + }, | |
| 3763 | + { | |
| 3764 | + "name": "Helen & Billie (Mia Kouppa)", | |
| 3765 | + "handles": { | |
| 3766 | + "instagram": "miakouppa", | |
| 3767 | + "tiktok": null, | |
| 3768 | + "youtube": null, | |
| 3769 | + "facebook": null, | |
| 3770 | + "x": null, | |
| 3771 | + "twitch": null | |
| 3772 | + }, | |
| 3773 | + "metrics": { | |
| 3774 | + "instagram_followers": 41000, | |
| 3775 | + "tiktok_followers": null, | |
| 3776 | + "youtube_subscribers": null, | |
| 3777 | + "facebook_followers": null, | |
| 3778 | + "x_followers": null, | |
| 3779 | + "twitch_followers": null | |
| 3780 | + }, | |
| 3781 | + "category": "food", | |
| 3782 | + "location": "Québec", | |
| 3783 | + "notes": "Recettes grecques authentiques familiales; miakouppa.com", | |
| 3784 | + "sources": [ | |
| 3785 | + "feedspot_food" | |
| 3786 | + ], | |
| 3787 | + "total_followers": 41000, | |
| 3788 | + "platform_count": 1, | |
| 3789 | + "tier": "micro (10K+)" | |
| 3790 | + }, | |
| 3791 | + { | |
| 3792 | + "name": "Sarah Maamari", | |
| 3793 | + "handles": { | |
| 3794 | + "instagram": "sisimaamari", | |
| 3795 | + "tiktok": null, | |
| 3796 | + "youtube": null, | |
| 3797 | + "facebook": null, | |
| 3798 | + "x": null, | |
| 3799 | + "twitch": null | |
| 3800 | + }, | |
| 3801 | + "metrics": { | |
| 3802 | + "instagram_followers": 32800, | |
| 3803 | + "tiktok_followers": 7600, | |
| 3804 | + "youtube_subscribers": null, | |
| 3805 | + "facebook_followers": null, | |
| 3806 | + "x_followers": null, | |
| 3807 | + "twitch_followers": null | |
| 3808 | + }, | |
| 3809 | + "category": "mode", | |
| 3810 | + "location": "Montréal", | |
| 3811 | + "notes": "mode, lifestyle, beauté, bien-être et voyage de luxe", | |
| 3812 | + "sources": [ | |
| 3813 | + "collabstr" | |
| 3814 | + ], | |
| 3815 | + "total_followers": 40400, | |
| 3816 | + "platform_count": 2, | |
| 3817 | + "tier": "micro (10K+)" | |
| 3818 | + }, | |
| 3819 | + { | |
| 3820 | + "name": "Jasmine Delia", | |
| 3821 | + "handles": { | |
| 3822 | + "instagram": "jazzdelia", | |
| 3823 | + "tiktok": null, | |
| 3824 | + "youtube": null, | |
| 3825 | + "facebook": null, | |
| 3826 | + "x": null, | |
| 3827 | + "twitch": null | |
| 3828 | + }, | |
| 3829 | + "metrics": { | |
| 3830 | + "instagram_followers": 37400, | |
| 3831 | + "tiktok_followers": null, | |
| 3832 | + "youtube_subscribers": null, | |
| 3833 | + "facebook_followers": null, | |
| 3834 | + "x_followers": null, | |
| 3835 | + "twitch_followers": null | |
| 3836 | + }, | |
| 3837 | + "category": "fitness", | |
| 3838 | + "location": "Montréal", | |
| 3839 | + "notes": "fitness, mode, lifestyle", | |
| 3840 | + "sources": [ | |
| 3841 | + "feedspot" | |
| 3842 | + ], | |
| 3843 | + "total_followers": 37400, | |
| 3844 | + "platform_count": 1, | |
| 3845 | + "tier": "micro (10K+)" | |
| 3846 | + }, | |
| 3847 | + { | |
| 3848 | + "name": "Martin Juneau", | |
| 3849 | + "handles": { | |
| 3850 | + "instagram": "martin_juneau", | |
| 3851 | + "tiktok": null, | |
| 3852 | + "youtube": null, | |
| 3853 | + "facebook": null, | |
| 3854 | + "x": null, | |
| 3855 | + "twitch": null | |
| 3856 | + }, | |
| 3857 | + "metrics": { | |
| 3858 | + "instagram_followers": 35500, | |
| 3859 | + "tiktok_followers": null, | |
| 3860 | + "youtube_subscribers": null, | |
| 3861 | + "facebook_followers": null, | |
| 3862 | + "x_followers": null, | |
| 3863 | + "twitch_followers": null | |
| 3864 | + }, | |
| 3865 | + "category": "food", | |
| 3866 | + "location": "Montréal", | |
| 3867 | + "notes": "Chef et entrepreneur, passionné d'œnologie; chiffres de 2019", | |
| 3868 | + "sources": [ | |
| 3869 | + "clark_food" | |
| 3870 | + ], | |
| 3871 | + "total_followers": 35500, | |
| 3872 | + "platform_count": 1, | |
| 3873 | + "tier": "micro (10K+)" | |
| 3874 | + }, | |
| 3875 | + { | |
| 3876 | + "name": "Kim Dandurand", | |
| 3877 | + "handles": { | |
| 3878 | + "instagram": "xkimy23", | |
| 3879 | + "tiktok": "xkimy23", | |
| 3880 | + "youtube": null, | |
| 3881 | + "facebook": null, | |
| 3882 | + "x": null, | |
| 3883 | + "twitch": null | |
| 3884 | + }, | |
| 3885 | + "metrics": { | |
| 3886 | + "instagram_followers": 14600, | |
| 3887 | + "tiktok_followers": 20400, | |
| 3888 | + "youtube_subscribers": null, | |
| 3889 | + "facebook_followers": null, | |
| 3890 | + "x_followers": null, | |
| 3891 | + "twitch_followers": null | |
| 3892 | + }, | |
| 3893 | + "category": "lifestyle", | |
| 3894 | + "location": "Québec", | |
| 3895 | + "notes": "lifestyle et animaux (Golden Retrievers, chats); Top Creator Collabstr | Contenu lifestyle et animaux (Golden Retrievers et chats). Top Creator Collabstr, note 5.0.", | |
| 3896 | + "sources": [ | |
| 3897 | + "collabstr", | |
| 3898 | + "collabstr_tiktok" | |
| 3899 | + ], | |
| 3900 | + "total_followers": 35000, | |
| 3901 | + "platform_count": 2, | |
| 3902 | + "tier": "micro (10K+)" | |
| 3903 | + }, | |
| 3904 | + { | |
| 3905 | + "name": "Alicia Waid", | |
| 3906 | + "handles": { | |
| 3907 | + "instagram": "aliciawaid", | |
| 3908 | + "tiktok": null, | |
| 3909 | + "youtube": null, | |
| 3910 | + "facebook": null, | |
| 3911 | + "x": null, | |
| 3912 | + "twitch": null | |
| 3913 | + }, | |
| 3914 | + "metrics": { | |
| 3915 | + "instagram_followers": 32300, | |
| 3916 | + "tiktok_followers": null, | |
| 3917 | + "youtube_subscribers": null, | |
| 3918 | + "facebook_followers": null, | |
| 3919 | + "x_followers": null, | |
| 3920 | + "twitch_followers": null | |
| 3921 | + }, | |
| 3922 | + "category": "lifestyle", | |
| 3923 | + "location": "Montréal", | |
| 3924 | + "notes": "slow living, photographie (@aliciawaidphotography)", | |
| 3925 | + "sources": [ | |
| 3926 | + "feedspot" | |
| 3927 | + ], | |
| 3928 | + "total_followers": 32300, | |
| 3929 | + "platform_count": 1, | |
| 3930 | + "tier": "micro (10K+)" | |
| 3931 | + }, | |
| 3932 | + { | |
| 3933 | + "name": "Gigi Madanipour", | |
| 3934 | + "handles": { | |
| 3935 | + "instagram": "gigimadanipour", | |
| 3936 | + "tiktok": null, | |
| 3937 | + "youtube": null, | |
| 3938 | + "facebook": null, | |
| 3939 | + "x": null, | |
| 3940 | + "twitch": null | |
| 3941 | + }, | |
| 3942 | + "metrics": { | |
| 3943 | + "instagram_followers": 30500, | |
| 3944 | + "tiktok_followers": null, | |
| 3945 | + "youtube_subscribers": null, | |
| 3946 | + "facebook_followers": null, | |
| 3947 | + "x_followers": null, | |
| 3948 | + "twitch_followers": null | |
| 3949 | + }, | |
| 3950 | + "category": "food", | |
| 3951 | + "location": "Montréal", | |
| 3952 | + "notes": "Soupers faciles et plats pour recevoir; contributrice Flavour Network; maman de 2", | |
| 3953 | + "sources": [ | |
| 3954 | + "feedspot_food" | |
| 3955 | + ], | |
| 3956 | + "total_followers": 30500, | |
| 3957 | + "platform_count": 1, | |
| 3958 | + "tier": "micro (10K+)" | |
| 3959 | + }, | |
| 3960 | + { | |
| 3961 | + "name": "Steve Daniel", | |
| 3962 | + "handles": { | |
| 3963 | + "instagram": "iamstevedaniel", | |
| 3964 | + "tiktok": null, | |
| 3965 | + "youtube": null, | |
| 3966 | + "facebook": null, | |
| 3967 | + "x": null, | |
| 3968 | + "twitch": null | |
| 3969 | + }, | |
| 3970 | + "metrics": { | |
| 3971 | + "instagram_followers": 28600, | |
| 3972 | + "tiktok_followers": null, | |
| 3973 | + "youtube_subscribers": null, | |
| 3974 | + "facebook_followers": null, | |
| 3975 | + "x_followers": null, | |
| 3976 | + "twitch_followers": null | |
| 3977 | + }, | |
| 3978 | + "category": "food", | |
| 3979 | + "location": "Montréal", | |
| 3980 | + "notes": "Food & expériences à Montréal; +25% de croissance IG en 3 mois", | |
| 3981 | + "sources": [ | |
| 3982 | + "feedspot_food" | |
| 3983 | + ], | |
| 3984 | + "total_followers": 28600, | |
| 3985 | + "platform_count": 1, | |
| 3986 | + "tier": "micro (10K+)" | |
| 3987 | + }, | |
| 3988 | + { | |
| 3989 | + "name": "Audrey Daigneault", | |
| 3990 | + "handles": { | |
| 3991 | + "instagram": "audrey_qc", | |
| 3992 | + "tiktok": null, | |
| 3993 | + "youtube": null, | |
| 3994 | + "facebook": null, | |
| 3995 | + "x": null, | |
| 3996 | + "twitch": null | |
| 3997 | + }, | |
| 3998 | + "metrics": { | |
| 3999 | + "instagram_followers": 28400, | |
| 4000 | + "tiktok_followers": null, | |
| 4001 | + "youtube_subscribers": null, | |
| 4002 | + "facebook_followers": null, | |
| 4003 | + "x_followers": null, | |
| 4004 | + "twitch_followers": null | |
| 4005 | + }, | |
| 4006 | + "category": "lifestyle", | |
| 4007 | + "location": "Québec", | |
| 4008 | + "notes": "créatrice québécoise ex-prof, maman, contenu sans filtre", | |
| 4009 | + "sources": [ | |
| 4010 | + "feedspot" | |
| 4011 | + ], | |
| 4012 | + "total_followers": 28400, | |
| 4013 | + "platform_count": 1, | |
| 4014 | + "tier": "micro (10K+)" | |
| 4015 | + }, | |
| 4016 | + { | |
| 4017 | + "name": "Gigi (Gigi in MTL)", | |
| 4018 | + "handles": { | |
| 4019 | + "instagram": "gigi.in.mtl", | |
| 4020 | + "tiktok": null, | |
| 4021 | + "youtube": null, | |
| 4022 | + "facebook": null, | |
| 4023 | + "x": null, | |
| 4024 | + "twitch": null | |
| 4025 | + }, | |
| 4026 | + "metrics": { | |
| 4027 | + "instagram_followers": 28100, | |
| 4028 | + "tiktok_followers": null, | |
| 4029 | + "youtube_subscribers": null, | |
| 4030 | + "facebook_followers": null, | |
| 4031 | + "x_followers": null, | |
| 4032 | + "twitch_followers": null | |
| 4033 | + }, | |
| 4034 | + "category": "food", | |
| 4035 | + "location": "Montréal", | |
| 4036 | + "notes": "Food, voyage et lifestyle FR/EN; 27 500 abonnés IG selon La Grappe", | |
| 4037 | + "sources": [ | |
| 4038 | + "feedspot_food", | |
| 4039 | + "lagrappe_food" | |
| 4040 | + ], | |
| 4041 | + "total_followers": 28100, | |
| 4042 | + "platform_count": 1, | |
| 4043 | + "tier": "micro (10K+)" | |
| 4044 | + }, | |
| 4045 | + { | |
| 4046 | + "name": "Jérémie Falissard", | |
| 4047 | + "handles": { | |
| 4048 | + "instagram": "jeremie_barroco", | |
| 4049 | + "tiktok": null, | |
| 4050 | + "youtube": null, | |
| 4051 | + "facebook": null, | |
| 4052 | + "x": null, | |
| 4053 | + "twitch": null | |
| 4054 | + }, | |
| 4055 | + "metrics": { | |
| 4056 | + "instagram_followers": 27600, | |
| 4057 | + "tiktok_followers": null, | |
| 4058 | + "youtube_subscribers": null, | |
| 4059 | + "facebook_followers": null, | |
| 4060 | + "x_followers": null, | |
| 4061 | + "twitch_followers": null | |
| 4062 | + }, | |
| 4063 | + "category": "food", | |
| 4064 | + "location": "Québec", | |
| 4065 | + "notes": "Chef/restaurateur, co-fondateur du Groupe Barroco, Top Chef France S14", | |
| 4066 | + "sources": [ | |
| 4067 | + "feedspot_food" | |
| 4068 | + ], | |
| 4069 | + "total_followers": 27600, | |
| 4070 | + "platform_count": 1, | |
| 4071 | + "tier": "micro (10K+)" | |
| 4072 | + }, | |
| 4073 | + { | |
| 4074 | + "name": "Natacha (Natacha Eats)", | |
| 4075 | + "handles": { | |
| 4076 | + "instagram": "natachaeats", | |
| 4077 | + "tiktok": null, | |
| 4078 | + "youtube": null, | |
| 4079 | + "facebook": null, | |
| 4080 | + "x": null, | |
| 4081 | + "twitch": null | |
| 4082 | + }, | |
| 4083 | + "metrics": { | |
| 4084 | + "instagram_followers": 27400, | |
| 4085 | + "tiktok_followers": null, | |
| 4086 | + "youtube_subscribers": null, | |
| 4087 | + "facebook_followers": null, | |
| 4088 | + "x_followers": null, | |
| 4089 | + "twitch_followers": null | |
| 4090 | + }, | |
| 4091 | + "category": "food", | |
| 4092 | + "location": "Québec", | |
| 4093 | + "notes": "Foodie, voyage et lifestyle; @shareyourplate.ca", | |
| 4094 | + "sources": [ | |
| 4095 | + "feedspot_food" | |
| 4096 | + ], | |
| 4097 | + "total_followers": 27400, | |
| 4098 | + "platform_count": 1, | |
| 4099 | + "tier": "micro (10K+)" | |
| 4100 | + }, | |
| 4101 | + { | |
| 4102 | + "name": "Nirina Andreana", | |
| 4103 | + "handles": { | |
| 4104 | + "instagram": "itisnirina", | |
| 4105 | + "tiktok": null, | |
| 4106 | + "youtube": null, | |
| 4107 | + "facebook": null, | |
| 4108 | + "x": null, | |
| 4109 | + "twitch": null | |
| 4110 | + }, | |
| 4111 | + "metrics": { | |
| 4112 | + "instagram_followers": 27200, | |
| 4113 | + "tiktok_followers": null, | |
| 4114 | + "youtube_subscribers": null, | |
| 4115 | + "facebook_followers": null, | |
| 4116 | + "x_followers": null, | |
| 4117 | + "twitch_followers": null | |
| 4118 | + }, | |
| 4119 | + "category": "lifestyle", | |
| 4120 | + "location": "Montréal", | |
| 4121 | + "notes": "lifestyle et shopping", | |
| 4122 | + "sources": [ | |
| 4123 | + "feedspot" | |
| 4124 | + ], | |
| 4125 | + "total_followers": 27200, | |
| 4126 | + "platform_count": 1, | |
| 4127 | + "tier": "micro (10K+)" | |
| 4128 | + }, | |
| 4129 | + { | |
| 4130 | + "name": "Salma Bousnina", | |
| 4131 | + "handles": { | |
| 4132 | + "instagram": "tmavy_salma", | |
| 4133 | + "tiktok": null, | |
| 4134 | + "youtube": null, | |
| 4135 | + "facebook": null, | |
| 4136 | + "x": null, | |
| 4137 | + "twitch": null | |
| 4138 | + }, | |
| 4139 | + "metrics": { | |
| 4140 | + "instagram_followers": 26900, | |
| 4141 | + "tiktok_followers": null, | |
| 4142 | + "youtube_subscribers": null, | |
| 4143 | + "facebook_followers": null, | |
| 4144 | + "x_followers": null, | |
| 4145 | + "twitch_followers": null | |
| 4146 | + }, | |
| 4147 | + "category": "mode", | |
| 4148 | + "location": "Québec", | |
| 4149 | + "notes": "maternité, mode, beauté; traductrice/interprète le jour", | |
| 4150 | + "sources": [ | |
| 4151 | + "feedspot" | |
| 4152 | + ], | |
| 4153 | + "total_followers": 26900, | |
| 4154 | + "platform_count": 1, | |
| 4155 | + "tier": "micro (10K+)" | |
| 4156 | + }, | |
| 4157 | + { | |
| 4158 | + "name": "Maire de Laval", | |
| 4159 | + "handles": { | |
| 4160 | + "instagram": "mairedelaval", | |
| 4161 | + "tiktok": null, | |
| 4162 | + "youtube": "maire de laval", | |
| 4163 | + "facebook": null, | |
| 4164 | + "x": null, | |
| 4165 | + "twitch": null | |
| 4166 | + }, | |
| 4167 | + "metrics": { | |
| 4168 | + "instagram_followers": 26800, | |
| 4169 | + "tiktok_followers": null, | |
| 4170 | + "youtube_subscribers": null, | |
| 4171 | + "facebook_followers": null, | |
| 4172 | + "x_followers": null, | |
| 4173 | + "twitch_followers": null | |
| 4174 | + }, | |
| 4175 | + "category": "autre", | |
| 4176 | + "location": "Laval, Québec", | |
| 4177 | + "notes": "Analyses culturelles et politiques au ton brut. Top YouTubeurs QC 2026 (ONEWAY).", | |
| 4178 | + "sources": [ | |
| 4179 | + "oneway_youtube" | |
| 4180 | + ], | |
| 4181 | + "total_followers": 26800, | |
| 4182 | + "platform_count": 2, | |
| 4183 | + "tier": "micro (10K+)" | |
| 4184 | + }, | |
| 4185 | + { | |
| 4186 | + "name": "Mathilde Chamberland", | |
| 4187 | + "handles": { | |
| 4188 | + "instagram": "mathildechamberland", | |
| 4189 | + "tiktok": null, | |
| 4190 | + "youtube": null, | |
| 4191 | + "facebook": null, | |
| 4192 | + "x": null, | |
| 4193 | + "twitch": null | |
| 4194 | + }, | |
| 4195 | + "metrics": { | |
| 4196 | + "instagram_followers": 26500, | |
| 4197 | + "tiktok_followers": null, | |
| 4198 | + "youtube_subscribers": null, | |
| 4199 | + "facebook_followers": null, | |
| 4200 | + "x_followers": null, | |
| 4201 | + "twitch_followers": null | |
| 4202 | + }, | |
| 4203 | + "category": "food", | |
| 4204 | + "location": "Québec", | |
| 4205 | + "notes": "Slow motherhood et vie à la campagne", | |
| 4206 | + "sources": [ | |
| 4207 | + "feedspot_food" | |
| 4208 | + ], | |
| 4209 | + "total_followers": 26500, | |
| 4210 | + "platform_count": 1, | |
| 4211 | + "tier": "micro (10K+)" | |
| 4212 | + }, | |
| 4213 | + { | |
| 4214 | + "name": "Jeremy Fillion", | |
| 4215 | + "handles": { | |
| 4216 | + "instagram": "jeremyfillion", | |
| 4217 | + "tiktok": "jeremyfillion", | |
| 4218 | + "youtube": null, | |
| 4219 | + "facebook": null, | |
| 4220 | + "x": null, | |
| 4221 | + "twitch": null | |
| 4222 | + }, | |
| 4223 | + "metrics": { | |
| 4224 | + "instagram_followers": 3300, | |
| 4225 | + "tiktok_followers": 21800, | |
| 4226 | + "youtube_subscribers": null, | |
| 4227 | + "facebook_followers": null, | |
| 4228 | + "x_followers": null, | |
| 4229 | + "twitch_followers": null | |
| 4230 | + }, | |
| 4231 | + "category": "lifestyle", | |
| 4232 | + "location": "Québec", | |
| 4233 | + "notes": "créateur franco-canadien, vlogs « Day in My Life » et beauté | Créateur franco-canadien : vlogs quotidiens, beauté (maquillage, soins, parfums). Apparaît deux fois sur Collabstr (21.8k et 21.5k).", | |
| 4234 | + "sources": [ | |
| 4235 | + "collabstr", | |
| 4236 | + "collabstr_tiktok" | |
| 4237 | + ], | |
| 4238 | + "total_followers": 25100, | |
| 4239 | + "platform_count": 2, | |
| 4240 | + "tier": "micro (10K+)" | |
| 4241 | + }, | |
| 4242 | + { | |
| 4243 | + "name": "Cassidy Neves", | |
| 4244 | + "handles": { | |
| 4245 | + "instagram": "cassidyneves", | |
| 4246 | + "tiktok": null, | |
| 4247 | + "youtube": null, | |
| 4248 | + "facebook": null, | |
| 4249 | + "x": null, | |
| 4250 | + "twitch": null | |
| 4251 | + }, | |
| 4252 | + "metrics": { | |
| 4253 | + "instagram_followers": 24400, | |
| 4254 | + "tiktok_followers": null, | |
| 4255 | + "youtube_subscribers": null, | |
| 4256 | + "facebook_followers": null, | |
| 4257 | + "x_followers": null, | |
| 4258 | + "twitch_followers": null | |
| 4259 | + }, | |
| 4260 | + "category": "lifestyle", | |
| 4261 | + "location": "Montréal", | |
| 4262 | + "notes": "outfits, livres et sports; e-commerce chez YSL Beauty / Armani Beauty", | |
| 4263 | + "sources": [ | |
| 4264 | + "feedspot" | |
| 4265 | + ], | |
| 4266 | + "total_followers": 24400, | |
| 4267 | + "platform_count": 1, | |
| 4268 | + "tier": "micro (10K+)" | |
| 4269 | + }, | |
| 4270 | + { | |
| 4271 | + "name": "Aretha (arethaeats)", | |
| 4272 | + "handles": { | |
| 4273 | + "instagram": "arethaeats", | |
| 4274 | + "tiktok": null, | |
| 4275 | + "youtube": null, | |
| 4276 | + "facebook": null, | |
| 4277 | + "x": null, | |
| 4278 | + "twitch": null | |
| 4279 | + }, | |
| 4280 | + "metrics": { | |
| 4281 | + "instagram_followers": 23900, | |
| 4282 | + "tiktok_followers": null, | |
| 4283 | + "youtube_subscribers": null, | |
| 4284 | + "facebook_followers": null, | |
| 4285 | + "x_followers": null, | |
| 4286 | + "twitch_followers": null | |
| 4287 | + }, | |
| 4288 | + "category": "food", | |
| 4289 | + "location": "Montréal", | |
| 4290 | + "notes": "Restaurants et activités à Montréal", | |
| 4291 | + "sources": [ | |
| 4292 | + "feedspot_food" | |
| 4293 | + ], | |
| 4294 | + "total_followers": 23900, | |
| 4295 | + "platform_count": 1, | |
| 4296 | + "tier": "micro (10K+)" | |
| 4297 | + }, | |
| 4298 | + { | |
| 4299 | + "name": "Nath (fulltimefoooodie)", | |
| 4300 | + "handles": { | |
| 4301 | + "instagram": "fulltimefoooodie", | |
| 4302 | + "tiktok": null, | |
| 4303 | + "youtube": null, | |
| 4304 | + "facebook": null, | |
| 4305 | + "x": null, | |
| 4306 | + "twitch": null | |
| 4307 | + }, | |
| 4308 | + "metrics": { | |
| 4309 | + "instagram_followers": 23700, | |
| 4310 | + "tiktok_followers": null, | |
| 4311 | + "youtube_subscribers": null, | |
| 4312 | + "facebook_followers": null, | |
| 4313 | + "x_followers": null, | |
| 4314 | + "twitch_followers": null | |
| 4315 | + }, | |
| 4316 | + "category": "food", | |
| 4317 | + "location": "Montréal", | |
| 4318 | + "notes": "Bouffe, drinks et activités partout à Montréal", | |
| 4319 | + "sources": [ | |
| 4320 | + "feedspot_food" | |
| 4321 | + ], | |
| 4322 | + "total_followers": 23700, | |
| 4323 | + "platform_count": 1, | |
| 4324 | + "tier": "micro (10K+)" | |
| 4325 | + }, | |
| 4326 | + { | |
| 4327 | + "name": "Molly Paquin", | |
| 4328 | + "handles": { | |
| 4329 | + "instagram": null, | |
| 4330 | + "tiktok": "mollypaquin_sobre", | |
| 4331 | + "youtube": null, | |
| 4332 | + "facebook": null, | |
| 4333 | + "x": null, | |
| 4334 | + "twitch": null | |
| 4335 | + }, | |
| 4336 | + "metrics": { | |
| 4337 | + "instagram_followers": null, | |
| 4338 | + "tiktok_followers": 22600, | |
| 4339 | + "youtube_subscribers": null, | |
| 4340 | + "facebook_followers": null, | |
| 4341 | + "x_followers": null, | |
| 4342 | + "twitch_followers": null | |
| 4343 | + }, | |
| 4344 | + "category": "lifestyle", | |
| 4345 | + "location": "Québec", | |
| 4346 | + "notes": "Créatrice lifestyle : vie de famille, astuces beauté et recettes.", | |
| 4347 | + "sources": [ | |
| 4348 | + "collabstr_tiktok" | |
| 4349 | + ], | |
| 4350 | + "total_followers": 22600, | |
| 4351 | + "platform_count": 1, | |
| 4352 | + "tier": "micro (10K+)" | |
| 4353 | + }, | |
| 4354 | + { | |
| 4355 | + "name": "Annie Ferland", | |
| 4356 | + "handles": { | |
| 4357 | + "instagram": "sciencefourchette", | |
| 4358 | + "tiktok": null, | |
| 4359 | + "youtube": null, | |
| 4360 | + "facebook": null, | |
| 4361 | + "x": null, | |
| 4362 | + "twitch": null | |
| 4363 | + }, | |
| 4364 | + "metrics": { | |
| 4365 | + "instagram_followers": 22400, | |
| 4366 | + "tiktok_followers": null, | |
| 4367 | + "youtube_subscribers": null, | |
| 4368 | + "facebook_followers": null, | |
| 4369 | + "x_followers": null, | |
| 4370 | + "twitch_followers": null | |
| 4371 | + }, | |
| 4372 | + "category": "food", | |
| 4373 | + "location": "Montréal", | |
| 4374 | + "notes": "Nutritionniste Dt.P., Ph.D., fondatrice de Science & Fourchette", | |
| 4375 | + "sources": [ | |
| 4376 | + "feedspot_food" | |
| 4377 | + ], | |
| 4378 | + "total_followers": 22400, | |
| 4379 | + "platform_count": 1, | |
| 4380 | + "tier": "micro (10K+)" | |
| 4381 | + }, | |
| 4382 | + { | |
| 4383 | + "name": "Murielle Banackissa", | |
| 4384 | + "handles": { | |
| 4385 | + "instagram": "muriellebanackissa", | |
| 4386 | + "tiktok": null, | |
| 4387 | + "youtube": null, | |
| 4388 | + "facebook": null, | |
| 4389 | + "x": null, | |
| 4390 | + "twitch": null | |
| 4391 | + }, | |
| 4392 | + "metrics": { | |
| 4393 | + "instagram_followers": 21600, | |
| 4394 | + "tiktok_followers": null, | |
| 4395 | + "youtube_subscribers": null, | |
| 4396 | + "facebook_followers": null, | |
| 4397 | + "x_followers": null, | |
| 4398 | + "twitch_followers": null | |
| 4399 | + }, | |
| 4400 | + "category": "food", | |
| 4401 | + "location": "Montréal", | |
| 4402 | + "notes": "Auteure de livre de cuisine végane (Savoring), photographe et styliste culinaire", | |
| 4403 | + "sources": [ | |
| 4404 | + "feedspot_food" | |
| 4405 | + ], | |
| 4406 | + "total_followers": 21600, | |
| 4407 | + "platform_count": 1, | |
| 4408 | + "tier": "micro (10K+)" | |
| 4409 | + }, | |
| 4410 | + { | |
| 4411 | + "name": "Genevieve Cusson", | |
| 4412 | + "handles": { | |
| 4413 | + "instagram": null, | |
| 4414 | + "tiktok": "genevieve.geteste", | |
| 4415 | + "youtube": null, | |
| 4416 | + "facebook": null, | |
| 4417 | + "x": null, | |
| 4418 | + "twitch": null | |
| 4419 | + }, | |
| 4420 | + "metrics": { | |
| 4421 | + "instagram_followers": null, | |
| 4422 | + "tiktok_followers": 20800, | |
| 4423 | + "youtube_subscribers": null, | |
| 4424 | + "facebook_followers": null, | |
| 4425 | + "x_followers": null, | |
| 4426 | + "twitch_followers": null | |
| 4427 | + }, | |
| 4428 | + "category": "lifestyle", | |
| 4429 | + "location": "Québec", | |
| 4430 | + "notes": "Tests et revues de produits, découvertes lifestyle, UGC.", | |
| 4431 | + "sources": [ | |
| 4432 | + "collabstr_tiktok" | |
| 4433 | + ], | |
| 4434 | + "total_followers": 20800, | |
| 4435 | + "platform_count": 1, | |
| 4436 | + "tier": "micro (10K+)" | |
| 4437 | + }, | |
| 4438 | + { | |
| 4439 | + "name": "Food Guy Montreal", | |
| 4440 | + "handles": { | |
| 4441 | + "instagram": "foodguymtl", | |
| 4442 | + "tiktok": null, | |
| 4443 | + "youtube": null, | |
| 4444 | + "facebook": null, | |
| 4445 | + "x": null, | |
| 4446 | + "twitch": null | |
| 4447 | + }, | |
| 4448 | + "metrics": { | |
| 4449 | + "instagram_followers": 20400, | |
| 4450 | + "tiktok_followers": null, | |
| 4451 | + "youtube_subscribers": null, | |
| 4452 | + "facebook_followers": null, | |
| 4453 | + "x_followers": null, | |
| 4454 | + "twitch_followers": null | |
| 4455 | + }, | |
| 4456 | + "category": "food", | |
| 4457 | + "location": "Montréal", | |
| 4458 | + "notes": "Source de tout ce qui est bouffe à Montréal", | |
| 4459 | + "sources": [ | |
| 4460 | + "feedspot_food" | |
| 4461 | + ], | |
| 4462 | + "total_followers": 20400, | |
| 4463 | + "platform_count": 1, | |
| 4464 | + "tier": "micro (10K+)" | |
| 4465 | + }, | |
| 4466 | + { | |
| 4467 | + "name": "Stefano Faita", | |
| 4468 | + "handles": { | |
| 4469 | + "instagram": "sfaita", | |
| 4470 | + "tiktok": null, | |
| 4471 | + "youtube": null, | |
| 4472 | + "facebook": null, | |
| 4473 | + "x": null, | |
| 4474 | + "twitch": null | |
| 4475 | + }, | |
| 4476 | + "metrics": { | |
| 4477 | + "instagram_followers": 20000, | |
| 4478 | + "tiktok_followers": null, | |
| 4479 | + "youtube_subscribers": null, | |
| 4480 | + "facebook_followers": null, | |
| 4481 | + "x_followers": null, | |
| 4482 | + "twitch_followers": null | |
| 4483 | + }, | |
| 4484 | + "category": "food", | |
| 4485 | + "location": "Montréal", | |
| 4486 | + "notes": "Roi de la cuisine italienne à Montréal; 3 restaurants avec Michele Forgione, pizzas et sauces; chiffres de 2019 (~20 000)", | |
| 4487 | + "sources": [ | |
| 4488 | + "clark_food" | |
| 4489 | + ], | |
| 4490 | + "total_followers": 20000, | |
| 4491 | + "platform_count": 1, | |
| 4492 | + "tier": "micro (10K+)" | |
| 4493 | + }, | |
| 4494 | + { | |
| 4495 | + "name": "Emily Doung", | |
| 4496 | + "handles": { | |
| 4497 | + "instagram": "ewilydee", | |
| 4498 | + "tiktok": null, | |
| 4499 | + "youtube": null, | |
| 4500 | + "facebook": null, | |
| 4501 | + "x": null, | |
| 4502 | + "twitch": null | |
| 4503 | + }, | |
| 4504 | + "metrics": { | |
| 4505 | + "instagram_followers": 19800, | |
| 4506 | + "tiktok_followers": null, | |
| 4507 | + "youtube_subscribers": null, | |
| 4508 | + "facebook_followers": null, | |
| 4509 | + "x_followers": null, | |
| 4510 | + "twitch_followers": null | |
| 4511 | + }, | |
| 4512 | + "category": "food", | |
| 4513 | + "location": "Montréal", | |
| 4514 | + "notes": "Médecin la semaine, créatrice de contenu la fin de semaine; posts quotidiens EN/FR", | |
| 4515 | + "sources": [ | |
| 4516 | + "feedspot_food" | |
| 4517 | + ], | |
| 4518 | + "total_followers": 19800, | |
| 4519 | + "platform_count": 1, | |
| 4520 | + "tier": "micro (10K+)" | |
| 4521 | + }, | |
| 4522 | + { | |
| 4523 | + "name": "Jessica (toutedansmesfesses)", | |
| 4524 | + "handles": { | |
| 4525 | + "instagram": "toutedansmesfesses", | |
| 4526 | + "tiktok": null, | |
| 4527 | + "youtube": null, | |
| 4528 | + "facebook": null, | |
| 4529 | + "x": null, | |
| 4530 | + "twitch": null | |
| 4531 | + }, | |
| 4532 | + "metrics": { | |
| 4533 | + "instagram_followers": 19600, | |
| 4534 | + "tiktok_followers": null, | |
| 4535 | + "youtube_subscribers": null, | |
| 4536 | + "facebook_followers": null, | |
| 4537 | + "x_followers": null, | |
| 4538 | + "twitch_followers": null | |
| 4539 | + }, | |
| 4540 | + "category": "food", | |
| 4541 | + "location": "Montréal", | |
| 4542 | + "notes": "Archives des munchies de @jessicaprdnc", | |
| 4543 | + "sources": [ | |
| 4544 | + "feedspot_food" | |
| 4545 | + ], | |
| 4546 | + "total_followers": 19600, | |
| 4547 | + "platform_count": 1, | |
| 4548 | + "tier": "micro (10K+)" | |
| 4549 | + }, | |
| 4550 | + { | |
| 4551 | + "name": "Parneet Dhillon", | |
| 4552 | + "handles": { | |
| 4553 | + "instagram": "parneeetk_", | |
| 4554 | + "tiktok": null, | |
| 4555 | + "youtube": null, | |
| 4556 | + "facebook": null, | |
| 4557 | + "x": null, | |
| 4558 | + "twitch": null | |
| 4559 | + }, | |
| 4560 | + "metrics": { | |
| 4561 | + "instagram_followers": 19100, | |
| 4562 | + "tiktok_followers": null, | |
| 4563 | + "youtube_subscribers": null, | |
| 4564 | + "facebook_followers": null, | |
| 4565 | + "x_followers": null, | |
| 4566 | + "twitch_followers": null | |
| 4567 | + }, | |
| 4568 | + "category": "lifestyle", | |
| 4569 | + "location": "Montréal", | |
| 4570 | + "notes": "micro-influenceuse montréalaise", | |
| 4571 | + "sources": [ | |
| 4572 | + "feedspot" | |
| 4573 | + ], | |
| 4574 | + "total_followers": 19100, | |
| 4575 | + "platform_count": 1, | |
| 4576 | + "tier": "micro (10K+)" | |
| 4577 | + }, | |
| 4578 | + { | |
| 4579 | + "name": "Karolane Rondeau", | |
| 4580 | + "handles": { | |
| 4581 | + "instagram": "karolanerondeau", | |
| 4582 | + "tiktok": null, | |
| 4583 | + "youtube": null, | |
| 4584 | + "facebook": null, | |
| 4585 | + "x": null, | |
| 4586 | + "twitch": null | |
| 4587 | + }, | |
| 4588 | + "metrics": { | |
| 4589 | + "instagram_followers": 18600, | |
| 4590 | + "tiktok_followers": null, | |
| 4591 | + "youtube_subscribers": null, | |
| 4592 | + "facebook_followers": null, | |
| 4593 | + "x_followers": null, | |
| 4594 | + "twitch_followers": null | |
| 4595 | + }, | |
| 4596 | + "category": "food", | |
| 4597 | + "location": "Montréal", | |
| 4598 | + "notes": "Créatrice voyage + food", | |
| 4599 | + "sources": [ | |
| 4600 | + "feedspot_food" | |
| 4601 | + ], | |
| 4602 | + "total_followers": 18600, | |
| 4603 | + "platform_count": 1, | |
| 4604 | + "tier": "micro (10K+)" | |
| 4605 | + }, | |
| 4606 | + { | |
| 4607 | + "name": "Audrey Roberge", | |
| 4608 | + "handles": { | |
| 4609 | + "instagram": "audreyy_x", | |
| 4610 | + "tiktok": null, | |
| 4611 | + "youtube": null, | |
| 4612 | + "facebook": null, | |
| 4613 | + "x": null, | |
| 4614 | + "twitch": null | |
| 4615 | + }, | |
| 4616 | + "metrics": { | |
| 4617 | + "instagram_followers": 18100, | |
| 4618 | + "tiktok_followers": null, | |
| 4619 | + "youtube_subscribers": null, | |
| 4620 | + "facebook_followers": null, | |
| 4621 | + "x_followers": null, | |
| 4622 | + "twitch_followers": null | |
| 4623 | + }, | |
| 4624 | + "category": "lifestyle", | |
| 4625 | + "location": "Québec", | |
| 4626 | + "notes": "travaille en assurances", | |
| 4627 | + "sources": [ | |
| 4628 | + "feedspot" | |
| 4629 | + ], | |
| 4630 | + "total_followers": 18100, | |
| 4631 | + "platform_count": 1, | |
| 4632 | + "tier": "micro (10K+)" | |
| 4633 | + }, | |
| 4634 | + { | |
| 4635 | + "name": "Amanda Quirion", | |
| 4636 | + "handles": { | |
| 4637 | + "instagram": null, | |
| 4638 | + "tiktok": "amandaquirion", | |
| 4639 | + "youtube": null, | |
| 4640 | + "facebook": null, | |
| 4641 | + "x": null, | |
| 4642 | + "twitch": null | |
| 4643 | + }, | |
| 4644 | + "metrics": { | |
| 4645 | + "instagram_followers": 4600, | |
| 4646 | + "tiktok_followers": 13300, | |
| 4647 | + "youtube_subscribers": null, | |
| 4648 | + "facebook_followers": null, | |
| 4649 | + "x_followers": null, | |
| 4650 | + "twitch_followers": null | |
| 4651 | + }, | |
| 4652 | + "category": "lifestyle", | |
| 4653 | + "location": "Québec", | |
| 4654 | + "notes": "Maman de jumeaux et enseignante : slow motherhood, cuisine, beauté, sport.", | |
| 4655 | + "sources": [ | |
| 4656 | + "collabstr_tiktok" | |
| 4657 | + ], | |
| 4658 | + "total_followers": 17900, | |
| 4659 | + "platform_count": 2, | |
| 4660 | + "tier": "micro (10K+)" | |
| 4661 | + }, | |
| 4662 | + { | |
| 4663 | + "name": "Abby Rose Dar Juan", | |
| 4664 | + "handles": { | |
| 4665 | + "instagram": "abbyyxxrose", | |
| 4666 | + "tiktok": "abbyyxxrose", | |
| 4667 | + "youtube": null, | |
| 4668 | + "facebook": null, | |
| 4669 | + "x": null, | |
| 4670 | + "twitch": null | |
| 4671 | + }, | |
| 4672 | + "metrics": { | |
| 4673 | + "instagram_followers": 4400, | |
| 4674 | + "tiktok_followers": 13400, | |
| 4675 | + "youtube_subscribers": null, | |
| 4676 | + "facebook_followers": null, | |
| 4677 | + "x_followers": null, | |
| 4678 | + "twitch_followers": null | |
| 4679 | + }, | |
| 4680 | + "category": "lifestyle", | |
| 4681 | + "location": "Montréal", | |
| 4682 | + "notes": "Philippine à Montréal, travail, culture et vie après les heures de bureau", | |
| 4683 | + "sources": [ | |
| 4684 | + "feedspot" | |
| 4685 | + ], | |
| 4686 | + "total_followers": 17800, | |
| 4687 | + "platform_count": 2, | |
| 4688 | + "tier": "micro (10K+)" | |
| 4689 | + }, | |
| 4690 | + { | |
| 4691 | + "name": "Amelie Montambeault", | |
| 4692 | + "handles": { | |
| 4693 | + "instagram": null, | |
| 4694 | + "tiktok": "amey.newera", | |
| 4695 | + "youtube": null, | |
| 4696 | + "facebook": null, | |
| 4697 | + "x": null, | |
| 4698 | + "twitch": null | |
| 4699 | + }, | |
| 4700 | + "metrics": { | |
| 4701 | + "instagram_followers": 8200, | |
| 4702 | + "tiktok_followers": 9600, | |
| 4703 | + "youtube_subscribers": null, | |
| 4704 | + "facebook_followers": null, | |
| 4705 | + "x_followers": null, | |
| 4706 | + "twitch_followers": null | |
| 4707 | + }, | |
| 4708 | + "category": "beauté", | |
| 4709 | + "location": "Québec", | |
| 4710 | + "notes": "Créatrice UGC de luxe : skincare, voyage, food et marketing; maman de trois enfants.", | |
| 4711 | + "sources": [ | |
| 4712 | + "collabstr_tiktok" | |
| 4713 | + ], | |
| 4714 | + "total_followers": 17800, | |
| 4715 | + "platform_count": 2, | |
| 4716 | + "tier": "micro (10K+)" | |
| 4717 | + }, | |
| 4718 | + { | |
| 4719 | + "name": "Myka (mykababyxx)", | |
| 4720 | + "handles": { | |
| 4721 | + "instagram": "mykababyxx", | |
| 4722 | + "tiktok": null, | |
| 4723 | + "youtube": null, | |
| 4724 | + "facebook": null, | |
| 4725 | + "x": null, | |
| 4726 | + "twitch": null | |
| 4727 | + }, | |
| 4728 | + "metrics": { | |
| 4729 | + "instagram_followers": 17500, | |
| 4730 | + "tiktok_followers": null, | |
| 4731 | + "youtube_subscribers": null, | |
| 4732 | + "facebook_followers": null, | |
| 4733 | + "x_followers": null, | |
| 4734 | + "twitch_followers": null | |
| 4735 | + }, | |
| 4736 | + "category": "beauté", | |
| 4737 | + "location": "Québec", | |
| 4738 | + "notes": "Top 20 Modash Québec, rang 1; propriétaire @nailzzbymyka (ongles); engagement 3.44%", | |
| 4739 | + "sources": [ | |
| 4740 | + "modash" | |
| 4741 | + ], | |
| 4742 | + "total_followers": 17500, | |
| 4743 | + "platform_count": 1, | |
| 4744 | + "tier": "micro (10K+)" | |
| 4745 | + }, | |
| 4746 | + { | |
| 4747 | + "name": "Victor (Random Cuisine)", | |
| 4748 | + "handles": { | |
| 4749 | + "instagram": "randomcuisine", | |
| 4750 | + "tiktok": null, | |
| 4751 | + "youtube": null, | |
| 4752 | + "facebook": null, | |
| 4753 | + "x": null, | |
| 4754 | + "twitch": null | |
| 4755 | + }, | |
| 4756 | + "metrics": { | |
| 4757 | + "instagram_followers": 17300, | |
| 4758 | + "tiktok_followers": null, | |
| 4759 | + "youtube_subscribers": null, | |
| 4760 | + "facebook_followers": null, | |
| 4761 | + "x_followers": null, | |
| 4762 | + "twitch_followers": null | |
| 4763 | + }, | |
| 4764 | + "category": "food", | |
| 4765 | + "location": "Montréal", | |
| 4766 | + "notes": "Physicien quantique, sommelier de thé, chroniqueur bouffe asiatique; finaliste Prix Lauriers 2023", | |
| 4767 | + "sources": [ | |
| 4768 | + "feedspot_food" | |
| 4769 | + ], | |
| 4770 | + "total_followers": 17300, | |
| 4771 | + "platform_count": 1, | |
| 4772 | + "tier": "micro (10K+)" | |
| 4773 | + }, | |
| 4774 | + { | |
| 4775 | + "name": "Nitika", | |
| 4776 | + "handles": { | |
| 4777 | + "instagram": "heyitsnitika", | |
| 4778 | + "tiktok": null, | |
| 4779 | + "youtube": null, | |
| 4780 | + "facebook": null, | |
| 4781 | + "x": null, | |
| 4782 | + "twitch": null | |
| 4783 | + }, | |
| 4784 | + "metrics": { | |
| 4785 | + "instagram_followers": 17100, | |
| 4786 | + "tiktok_followers": null, | |
| 4787 | + "youtube_subscribers": null, | |
| 4788 | + "facebook_followers": null, | |
| 4789 | + "x_followers": null, | |
| 4790 | + "twitch_followers": null | |
| 4791 | + }, | |
| 4792 | + "category": "lifestyle", | |
| 4793 | + "location": "Montréal", | |
| 4794 | + "notes": "maman montréalaise de 40 ans, aventures/mode/bien-être (listée « Nikita » sur Feedspot)", | |
| 4795 | + "sources": [ | |
| 4796 | + "feedspot" | |
| 4797 | + ], | |
| 4798 | + "total_followers": 17100, | |
| 4799 | + "platform_count": 1, | |
| 4800 | + "tier": "micro (10K+)" | |
| 4801 | + }, | |
| 4802 | + { | |
| 4803 | + "name": "Evan (evanvanya)", | |
| 4804 | + "handles": { | |
| 4805 | + "instagram": "evanvanya", | |
| 4806 | + "tiktok": null, | |
| 4807 | + "youtube": null, | |
| 4808 | + "facebook": null, | |
| 4809 | + "x": null, | |
| 4810 | + "twitch": null | |
| 4811 | + }, | |
| 4812 | + "metrics": { | |
| 4813 | + "instagram_followers": 17100, | |
| 4814 | + "tiktok_followers": null, | |
| 4815 | + "youtube_subscribers": null, | |
| 4816 | + "facebook_followers": null, | |
| 4817 | + "x_followers": null, | |
| 4818 | + "twitch_followers": null | |
| 4819 | + }, | |
| 4820 | + "category": "food", | |
| 4821 | + "location": "Montréal", | |
| 4822 | + "notes": "Ne publie que ce qu'il a aimé", | |
| 4823 | + "sources": [ | |
| 4824 | + "feedspot_food" | |
| 4825 | + ], | |
| 4826 | + "total_followers": 17100, | |
| 4827 | + "platform_count": 1, | |
| 4828 | + "tier": "micro (10K+)" | |
| 4829 | + }, | |
| 4830 | + { | |
| 4831 | + "name": "Kenza Elkad", | |
| 4832 | + "handles": { | |
| 4833 | + "instagram": "kenzaelkad", | |
| 4834 | + "tiktok": null, | |
| 4835 | + "youtube": null, | |
| 4836 | + "facebook": null, | |
| 4837 | + "x": null, | |
| 4838 | + "twitch": null | |
| 4839 | + }, | |
| 4840 | + "metrics": { | |
| 4841 | + "instagram_followers": 17000, | |
| 4842 | + "tiktok_followers": null, | |
| 4843 | + "youtube_subscribers": null, | |
| 4844 | + "facebook_followers": null, | |
| 4845 | + "x_followers": null, | |
| 4846 | + "twitch_followers": null | |
| 4847 | + }, | |
| 4848 | + "category": "mode", | |
| 4849 | + "location": "Montréal", | |
| 4850 | + "notes": "ingénieure en bâtiment passionnée de mode", | |
| 4851 | + "sources": [ | |
| 4852 | + "feedspot" | |
| 4853 | + ], | |
| 4854 | + "total_followers": 17000, | |
| 4855 | + "platform_count": 1, | |
| 4856 | + "tier": "micro (10K+)" | |
| 4857 | + }, | |
| 4858 | + { | |
| 4859 | + "name": "Kim Williams", | |
| 4860 | + "handles": { | |
| 4861 | + "instagram": "kimoune", | |
| 4862 | + "tiktok": null, | |
| 4863 | + "youtube": null, | |
| 4864 | + "facebook": null, | |
| 4865 | + "x": null, | |
| 4866 | + "twitch": null | |
| 4867 | + }, | |
| 4868 | + "metrics": { | |
| 4869 | + "instagram_followers": 16900, | |
| 4870 | + "tiktok_followers": null, | |
| 4871 | + "youtube_subscribers": null, | |
| 4872 | + "facebook_followers": null, | |
| 4873 | + "x_followers": null, | |
| 4874 | + "twitch_followers": null | |
| 4875 | + }, | |
| 4876 | + "category": "lifestyle", | |
| 4877 | + "location": "Montréal", | |
| 4878 | + "notes": "animatrice radio (Énergie 94.3, Rouge 107.3), maman; lifestyle/mode/maternité", | |
| 4879 | + "sources": [ | |
| 4880 | + "feedspot", | |
| 4881 | + "embold" | |
| 4882 | + ], | |
| 4883 | + "total_followers": 16900, | |
| 4884 | + "platform_count": 1, | |
| 4885 | + "tier": "micro (10K+)" | |
| 4886 | + }, | |
| 4887 | + { | |
| 4888 | + "name": "Johanna Dalmeida (Ayélé)", | |
| 4889 | + "handles": { | |
| 4890 | + "instagram": null, | |
| 4891 | + "tiktok": "ayl.d", | |
| 4892 | + "youtube": null, | |
| 4893 | + "facebook": null, | |
| 4894 | + "x": null, | |
| 4895 | + "twitch": null | |
| 4896 | + }, | |
| 4897 | + "metrics": { | |
| 4898 | + "instagram_followers": 1300, | |
| 4899 | + "tiktok_followers": 15500, | |
| 4900 | + "youtube_subscribers": null, | |
| 4901 | + "facebook_followers": null, | |
| 4902 | + "x_followers": null, | |
| 4903 | + "twitch_followers": null | |
| 4904 | + }, | |
| 4905 | + "category": "beauté", | |
| 4906 | + "location": "Québec", | |
| 4907 | + "notes": "Créatrice de contenu beauté, lifestyle et découverte, 25 ans.", | |
| 4908 | + "sources": [ | |
| 4909 | + "collabstr_tiktok" | |
| 4910 | + ], | |
| 4911 | + "total_followers": 16800, | |
| 4912 | + "platform_count": 2, | |
| 4913 | + "tier": "micro (10K+)" | |
| 4914 | + }, | |
| 4915 | + { | |
| 4916 | + "name": "Shaida Rouamba", | |
| 4917 | + "handles": { | |
| 4918 | + "instagram": null, | |
| 4919 | + "tiktok": "urfavshxida", | |
| 4920 | + "youtube": null, | |
| 4921 | + "facebook": null, | |
| 4922 | + "x": null, | |
| 4923 | + "twitch": null | |
| 4924 | + }, | |
| 4925 | + "metrics": { | |
| 4926 | + "instagram_followers": 14700, | |
| 4927 | + "tiktok_followers": 1900, | |
| 4928 | + "youtube_subscribers": null, | |
| 4929 | + "facebook_followers": null, | |
| 4930 | + "x_followers": null, | |
| 4931 | + "twitch_followers": null | |
| 4932 | + }, | |
| 4933 | + "category": "mode", | |
| 4934 | + "location": "Québec", | |
| 4935 | + "notes": "Créatrice mode axée sur la confiance en soi et les tendances.", | |
| 4936 | + "sources": [ | |
| 4937 | + "collabstr_tiktok" | |
| 4938 | + ], | |
| 4939 | + "total_followers": 16600, | |
| 4940 | + "platform_count": 2, | |
| 4941 | + "tier": "micro (10K+)" | |
| 4942 | + }, | |
| 4943 | + { | |
| 4944 | + "name": "Aya Hayek", | |
| 4945 | + "handles": { | |
| 4946 | + "instagram": "ayahayek_", | |
| 4947 | + "tiktok": null, | |
| 4948 | + "youtube": null, | |
| 4949 | + "facebook": null, | |
| 4950 | + "x": null, | |
| 4951 | + "twitch": null | |
| 4952 | + }, | |
| 4953 | + "metrics": { | |
| 4954 | + "instagram_followers": 16200, | |
| 4955 | + "tiktok_followers": null, | |
| 4956 | + "youtube_subscribers": null, | |
| 4957 | + "facebook_followers": null, | |
| 4958 | + "x_followers": null, | |
| 4959 | + "twitch_followers": null | |
| 4960 | + }, | |
| 4961 | + "category": "food", | |
| 4962 | + "location": "Montréal", | |
| 4963 | + "notes": "créatrice lifestyle & food, UGC (@ayas.feedd)", | |
| 4964 | + "sources": [ | |
| 4965 | + "feedspot" | |
| 4966 | + ], | |
| 4967 | + "total_followers": 16200, | |
| 4968 | + "platform_count": 1, | |
| 4969 | + "tier": "micro (10K+)" | |
| 4970 | + }, | |
| 4971 | + { | |
| 4972 | + "name": "Caroline Elie", | |
| 4973 | + "handles": { | |
| 4974 | + "instagram": "caroline.elie", | |
| 4975 | + "tiktok": null, | |
| 4976 | + "youtube": null, | |
| 4977 | + "facebook": null, | |
| 4978 | + "x": null, | |
| 4979 | + "twitch": null | |
| 4980 | + }, | |
| 4981 | + "metrics": { | |
| 4982 | + "instagram_followers": 16000, | |
| 4983 | + "tiktok_followers": null, | |
| 4984 | + "youtube_subscribers": null, | |
| 4985 | + "facebook_followers": null, | |
| 4986 | + "x_followers": null, | |
| 4987 | + "twitch_followers": null | |
| 4988 | + }, | |
| 4989 | + "category": "lifestyle", | |
| 4990 | + "location": "Montréal", | |
| 4991 | + "notes": "experte luxe au quotidien, fondatrice @theeverydayluxury", | |
| 4992 | + "sources": [ | |
| 4993 | + "feedspot" | |
| 4994 | + ], | |
| 4995 | + "total_followers": 16000, | |
| 4996 | + "platform_count": 1, | |
| 4997 | + "tier": "micro (10K+)" | |
| 4998 | + }, | |
| 4999 | + { | |
| 5000 | + "name": "Jess (j3ss.food)", | |
| 5001 | + "handles": { | |
| 5002 | + "instagram": "j3ss.food", | |
| 5003 | + "tiktok": null, | |
| 5004 | + "youtube": null, | |
| 5005 | + "facebook": null, | |
| 5006 | + "x": null, | |
| 5007 | + "twitch": null | |
| 5008 | + }, | |
| 5009 | + "metrics": { | |
| 5010 | + "instagram_followers": 15700, | |
| 5011 | + "tiktok_followers": null, | |
| 5012 | + "youtube_subscribers": null, | |
| 5013 | + "facebook_followers": null, | |
| 5014 | + "x_followers": null, | |
| 5015 | + "twitch_followers": null | |
| 5016 | + }, | |
| 5017 | + "category": "food", | |
| 5018 | + "location": "Montréal", | |
| 5019 | + "notes": "Food et voyage entre Manchester et Montréal", | |
| 5020 | + "sources": [ | |
| 5021 | + "feedspot_food" | |
| 5022 | + ], | |
| 5023 | + "total_followers": 15700, | |
| 5024 | + "platform_count": 1, | |
| 5025 | + "tier": "micro (10K+)" | |
| 5026 | + }, | |
| 5027 | + { | |
| 5028 | + "name": "Ishman Kaur", | |
| 5029 | + "handles": { | |
| 5030 | + "instagram": "ishman.in.mtl", | |
| 5031 | + "tiktok": null, | |
| 5032 | + "youtube": null, | |
| 5033 | + "facebook": null, | |
| 5034 | + "x": null, | |
| 5035 | + "twitch": null | |
| 5036 | + }, | |
| 5037 | + "metrics": { | |
| 5038 | + "instagram_followers": 15600, | |
| 5039 | + "tiktok_followers": null, | |
| 5040 | + "youtube_subscribers": null, | |
| 5041 | + "facebook_followers": null, | |
| 5042 | + "x_followers": null, | |
| 5043 | + "twitch_followers": null | |
| 5044 | + }, | |
| 5045 | + "category": "mode", | |
| 5046 | + "location": "Montréal", | |
| 5047 | + "notes": "sciences de la vie + mode et lifestyle, recherche en oncologie", | |
| 5048 | + "sources": [ | |
| 5049 | + "feedspot" | |
| 5050 | + ], | |
| 5051 | + "total_followers": 15600, | |
| 5052 | + "platform_count": 1, | |
| 5053 | + "tier": "micro (10K+)" | |
| 5054 | + }, | |
| 5055 | + { | |
| 5056 | + "name": "Julia (julia_tastes)", | |
| 5057 | + "handles": { | |
| 5058 | + "instagram": "julia_tastes", | |
| 5059 | + "tiktok": null, | |
| 5060 | + "youtube": null, | |
| 5061 | + "facebook": null, | |
| 5062 | + "x": null, | |
| 5063 | + "twitch": null | |
| 5064 | + }, | |
| 5065 | + "metrics": { | |
| 5066 | + "instagram_followers": 15400, | |
| 5067 | + "tiktok_followers": null, | |
| 5068 | + "youtube_subscribers": null, | |
| 5069 | + "facebook_followers": null, | |
| 5070 | + "x_followers": null, | |
| 5071 | + "twitch_followers": null | |
| 5072 | + }, | |
| 5073 | + "category": "food", | |
| 5074 | + "location": "Montréal", | |
| 5075 | + "notes": "Latina à Montréal; food, lifestyle, cuisine EN/FR/ESP; créatrice UGC", | |
| 5076 | + "sources": [ | |
| 5077 | + "feedspot_food" | |
| 5078 | + ], | |
| 5079 | + "total_followers": 15400, | |
| 5080 | + "platform_count": 1, | |
| 5081 | + "tier": "micro (10K+)" | |
| 5082 | + }, | |
| 5083 | + { | |
| 5084 | + "name": "Dwyane101", | |
| 5085 | + "handles": { | |
| 5086 | + "instagram": null, | |
| 5087 | + "tiktok": null, | |
| 5088 | + "youtube": null, | |
| 5089 | + "facebook": null, | |
| 5090 | + "x": null, | |
| 5091 | + "twitch": "dwyane101" | |
| 5092 | + }, | |
| 5093 | + "metrics": { | |
| 5094 | + "instagram_followers": 1600, | |
| 5095 | + "tiktok_followers": 13100, | |
| 5096 | + "youtube_subscribers": 316, | |
| 5097 | + "facebook_followers": null, | |
| 5098 | + "x_followers": null, | |
| 5099 | + "twitch_followers": 380 | |
| 5100 | + }, | |
| 5101 | + "category": "gaming", | |
| 5102 | + "location": "Lachine, Québec", | |
| 5103 | + "notes": "Skits Roblox et clips Twitch, 19 ans", | |
| 5104 | + "sources": [ | |
| 5105 | + "collabstr_twitch" | |
| 5106 | + ], | |
| 5107 | + "total_followers": 15396, | |
| 5108 | + "platform_count": 4, | |
| 5109 | + "tier": "micro (10K+)" | |
| 5110 | + }, | |
| 5111 | + { | |
| 5112 | + "name": "Massimo Piedimonte", | |
| 5113 | + "handles": { | |
| 5114 | + "instagram": "massimoagogo", | |
| 5115 | + "tiktok": null, | |
| 5116 | + "youtube": null, | |
| 5117 | + "facebook": null, | |
| 5118 | + "x": null, | |
| 5119 | + "twitch": null | |
| 5120 | + }, | |
| 5121 | + "metrics": { | |
| 5122 | + "instagram_followers": 15300, | |
| 5123 | + "tiktok_followers": null, | |
| 5124 | + "youtube_subscribers": null, | |
| 5125 | + "facebook_followers": null, | |
| 5126 | + "x_followers": null, | |
| 5127 | + "twitch_followers": null | |
| 5128 | + }, | |
| 5129 | + "category": "food", | |
| 5130 | + "location": "Montréal", | |
| 5131 | + "notes": "Demi-finaliste Top Chef (Bravo US), proprio Cabaret l'Enfer; #2 Best New Restaurant Canada's 100 Best", | |
| 5132 | + "sources": [ | |
| 5133 | + "feedspot_food" | |
| 5134 | + ], | |
| 5135 | + "total_followers": 15300, | |
| 5136 | + "platform_count": 1, | |
| 5137 | + "tier": "micro (10K+)" | |
| 5138 | + }, | |
| 5139 | + { | |
| 5140 | + "name": "Serena Kani", | |
| 5141 | + "handles": { | |
| 5142 | + "instagram": "serenakani", | |
| 5143 | + "tiktok": null, | |
| 5144 | + "youtube": null, | |
| 5145 | + "facebook": null, | |
| 5146 | + "x": null, | |
| 5147 | + "twitch": null | |
| 5148 | + }, | |
| 5149 | + "metrics": { | |
| 5150 | + "instagram_followers": 14400, | |
| 5151 | + "tiktok_followers": null, | |
| 5152 | + "youtube_subscribers": null, | |
| 5153 | + "facebook_followers": null, | |
| 5154 | + "x_followers": null, | |
| 5155 | + "twitch_followers": null | |
| 5156 | + }, | |
| 5157 | + "category": "lifestyle", | |
| 5158 | + "location": "Montréal", | |
| 5159 | + "notes": "créatrice UGC, lifestyle et fitness", | |
| 5160 | + "sources": [ | |
| 5161 | + "feedspot" | |
| 5162 | + ], | |
| 5163 | + "total_followers": 14400, | |
| 5164 | + "platform_count": 1, | |
| 5165 | + "tier": "micro (10K+)" | |
| 5166 | + }, | |
| 5167 | + { | |
| 5168 | + "name": "Marie Morrissette", | |
| 5169 | + "handles": { | |
| 5170 | + "instagram": null, | |
| 5171 | + "tiktok": "mmorrissette16", | |
| 5172 | + "youtube": null, | |
| 5173 | + "facebook": null, | |
| 5174 | + "x": null, | |
| 5175 | + "twitch": null | |
| 5176 | + }, | |
| 5177 | + "metrics": { | |
| 5178 | + "instagram_followers": null, | |
| 5179 | + "tiktok_followers": 14100, | |
| 5180 | + "youtube_subscribers": null, | |
| 5181 | + "facebook_followers": null, | |
| 5182 | + "x_followers": null, | |
| 5183 | + "twitch_followers": null | |
| 5184 | + }, | |
| 5185 | + "category": "lifestyle", | |
| 5186 | + "location": "Québec", | |
| 5187 | + "notes": "Lifestyle, beauté et voyage; Instagram entre 10k et 50k abonnés.", | |
| 5188 | + "sources": [ | |
| 5189 | + "collabstr_tiktok" | |
| 5190 | + ], | |
| 5191 | + "total_followers": 14100, | |
| 5192 | + "platform_count": 1, | |
| 5193 | + "tier": "micro (10K+)" | |
| 5194 | + }, | |
| 5195 | + { | |
| 5196 | + "name": "Pascal Barriault", | |
| 5197 | + "handles": { | |
| 5198 | + "instagram": "pascalbarriault", | |
| 5199 | + "tiktok": null, | |
| 5200 | + "youtube": null, | |
| 5201 | + "facebook": null, | |
| 5202 | + "x": null, | |
| 5203 | + "twitch": null | |
| 5204 | + }, | |
| 5205 | + "metrics": { | |
| 5206 | + "instagram_followers": 14000, | |
| 5207 | + "tiktok_followers": null, | |
| 5208 | + "youtube_subscribers": null, | |
| 5209 | + "facebook_followers": null, | |
| 5210 | + "x_followers": null, | |
| 5211 | + "twitch_followers": null | |
| 5212 | + }, | |
| 5213 | + "category": "voyage", | |
| 5214 | + "location": "Québec", | |
| 5215 | + "notes": "Comédien; voyage entre tournages et spectacles (périple au Nicaragua)", | |
| 5216 | + "sources": [ | |
| 5217 | + "noovomoi_voyage" | |
| 5218 | + ], | |
| 5219 | + "total_followers": 14000, | |
| 5220 | + "platform_count": 1, | |
| 5221 | + "tier": "micro (10K+)" | |
| 5222 | + }, | |
| 5223 | + { | |
| 5224 | + "name": "Pascale Robitaille", | |
| 5225 | + "handles": { | |
| 5226 | + "instagram": "pascalerobitaille", | |
| 5227 | + "tiktok": null, | |
| 5228 | + "youtube": null, | |
| 5229 | + "facebook": null, | |
| 5230 | + "x": null, | |
| 5231 | + "twitch": null | |
| 5232 | + }, | |
| 5233 | + "metrics": { | |
| 5234 | + "instagram_followers": 13700, | |
| 5235 | + "tiktok_followers": null, | |
| 5236 | + "youtube_subscribers": null, | |
| 5237 | + "facebook_followers": null, | |
| 5238 | + "x_followers": null, | |
| 5239 | + "twitch_followers": null | |
| 5240 | + }, | |
| 5241 | + "category": "autre", | |
| 5242 | + "location": "Québec", | |
| 5243 | + "notes": "Top 20 Modash Québec, rang 2; journaliste TVA Nouvelles / Salut Bonjour; engagement 4.14%", | |
| 5244 | + "sources": [ | |
| 5245 | + "modash" | |
| 5246 | + ], | |
| 5247 | + "total_followers": 13700, | |
| 5248 | + "platform_count": 1, | |
| 5249 | + "tier": "micro (10K+)" | |
| 5250 | + }, | |
| 5251 | + { | |
| 5252 | + "name": "Soufiane Benrqiq", | |
| 5253 | + "handles": { | |
| 5254 | + "instagram": "soufiane", | |
| 5255 | + "tiktok": null, | |
| 5256 | + "youtube": null, | |
| 5257 | + "facebook": null, | |
| 5258 | + "x": null, | |
| 5259 | + "twitch": null | |
| 5260 | + }, | |
| 5261 | + "metrics": { | |
| 5262 | + "instagram_followers": 13300, | |
| 5263 | + "tiktok_followers": null, | |
| 5264 | + "youtube_subscribers": null, | |
| 5265 | + "facebook_followers": null, | |
| 5266 | + "x_followers": null, | |
| 5267 | + "twitch_followers": null | |
| 5268 | + }, | |
| 5269 | + "category": "musique", | |
| 5270 | + "location": "Québec", | |
| 5271 | + "notes": "Top 20 Modash Québec, rang 3; réalisateur / producteur musical (@upawasessions); engagement 1.09%", | |
| 5272 | + "sources": [ | |
| 5273 | + "modash" | |
| 5274 | + ], | |
| 5275 | + "total_followers": 13300, | |
| 5276 | + "platform_count": 1, | |
| 5277 | + "tier": "micro (10K+)" | |
| 5278 | + }, | |
| 5279 | + { | |
| 5280 | + "name": "Jeannie (jeanniesfood)", | |
| 5281 | + "handles": { | |
| 5282 | + "instagram": "jeanniesfood", | |
| 5283 | + "tiktok": null, | |
| 5284 | + "youtube": null, | |
| 5285 | + "facebook": null, | |
| 5286 | + "x": null, | |
| 5287 | + "twitch": null | |
| 5288 | + }, | |
| 5289 | + "metrics": { | |
| 5290 | + "instagram_followers": 13300, | |
| 5291 | + "tiktok_followers": null, | |
| 5292 | + "youtube_subscribers": null, | |
| 5293 | + "facebook_followers": null, | |
| 5294 | + "x_followers": null, | |
| 5295 | + "twitch_followers": null | |
| 5296 | + }, | |
| 5297 | + "category": "food", | |
| 5298 | + "location": "Montréal", | |
| 5299 | + "notes": "Meilleurs restos et activités à Montréal", | |
| 5300 | + "sources": [ | |
| 5301 | + "feedspot_food" | |
| 5302 | + ], | |
| 5303 | + "total_followers": 13300, | |
| 5304 | + "platform_count": 1, | |
| 5305 | + "tier": "micro (10K+)" | |
| 5306 | + }, | |
| 5307 | + { | |
| 5308 | + "name": "Cynthia Marcotte", | |
| 5309 | + "handles": { | |
| 5310 | + "instagram": "cynthia.marcotte.dtp", | |
| 5311 | + "tiktok": null, | |
| 5312 | + "youtube": null, | |
| 5313 | + "facebook": null, | |
| 5314 | + "x": null, | |
| 5315 | + "twitch": null | |
| 5316 | + }, | |
| 5317 | + "metrics": { | |
| 5318 | + "instagram_followers": null, | |
| 5319 | + "tiktok_followers": null, | |
| 5320 | + "youtube_subscribers": 13300, | |
| 5321 | + "facebook_followers": null, | |
| 5322 | + "x_followers": null, | |
| 5323 | + "twitch_followers": null | |
| 5324 | + }, | |
| 5325 | + "category": "food", | |
| 5326 | + "location": "Québec", | |
| 5327 | + "notes": "Nutritionniste; prix pour ses vidéos diététiques au Gala DUX de Montréal; chiffres de 2019", | |
| 5328 | + "sources": [ | |
| 5329 | + "clark_food" | |
| 5330 | + ], | |
| 5331 | + "total_followers": 13300, | |
| 5332 | + "platform_count": 2, | |
| 5333 | + "tier": "micro (10K+)" | |
| 5334 | + }, | |
| 5335 | + { | |
| 5336 | + "name": "Marie-Julie Gagnon", | |
| 5337 | + "handles": { | |
| 5338 | + "instagram": "mariejuliega", | |
| 5339 | + "tiktok": null, | |
| 5340 | + "youtube": null, | |
| 5341 | + "facebook": null, | |
| 5342 | + "x": null, | |
| 5343 | + "twitch": null | |
| 5344 | + }, | |
| 5345 | + "metrics": { | |
| 5346 | + "instagram_followers": 13000, | |
| 5347 | + "tiktok_followers": null, | |
| 5348 | + "youtube_subscribers": null, | |
| 5349 | + "facebook_followers": null, | |
| 5350 | + "x_followers": null, | |
| 5351 | + "twitch_followers": null | |
| 5352 | + }, | |
| 5353 | + "category": "voyage", | |
| 5354 | + "location": "Québec", | |
| 5355 | + "notes": "Journaliste et chroniqueuse voyage (télé/radio); aussi @technomade", | |
| 5356 | + "sources": [ | |
| 5357 | + "noovomoi_voyage" | |
| 5358 | + ], | |
| 5359 | + "total_followers": 13000, | |
| 5360 | + "platform_count": 1, | |
| 5361 | + "tier": "micro (10K+)" | |
| 5362 | + }, | |
| 5363 | + { | |
| 5364 | + "name": "Mary Swaydan", | |
| 5365 | + "handles": { | |
| 5366 | + "instagram": "mary.swaydan", | |
| 5367 | + "tiktok": null, | |
| 5368 | + "youtube": null, | |
| 5369 | + "facebook": null, | |
| 5370 | + "x": null, | |
| 5371 | + "twitch": null | |
| 5372 | + }, | |
| 5373 | + "metrics": { | |
| 5374 | + "instagram_followers": 12900, | |
| 5375 | + "tiktok_followers": null, | |
| 5376 | + "youtube_subscribers": null, | |
| 5377 | + "facebook_followers": null, | |
| 5378 | + "x_followers": null, | |
| 5379 | + "twitch_followers": null | |
| 5380 | + }, | |
| 5381 | + "category": "lifestyle", | |
| 5382 | + "location": "Québec", | |
| 5383 | + "notes": "maman, vie à Montréal et au Saguenay–Lac-Saint-Jean, slow living et tourisme local", | |
| 5384 | + "sources": [ | |
| 5385 | + "feedspot" | |
| 5386 | + ], | |
| 5387 | + "total_followers": 12900, | |
| 5388 | + "platform_count": 1, | |
| 5389 | + "tier": "micro (10K+)" | |
| 5390 | + }, | |
| 5391 | + { | |
| 5392 | + "name": "Elodie Taylor", | |
| 5393 | + "handles": { | |
| 5394 | + "instagram": "elodie_taylor", | |
| 5395 | + "tiktok": "elodie_taylor", | |
| 5396 | + "youtube": null, | |
| 5397 | + "facebook": null, | |
| 5398 | + "x": null, | |
| 5399 | + "twitch": null | |
| 5400 | + }, | |
| 5401 | + "metrics": { | |
| 5402 | + "instagram_followers": 12700, | |
| 5403 | + "tiktok_followers": null, | |
| 5404 | + "youtube_subscribers": null, | |
| 5405 | + "facebook_followers": null, | |
| 5406 | + "x_followers": null, | |
| 5407 | + "twitch_followers": null | |
| 5408 | + }, | |
| 5409 | + "category": "beauté", | |
| 5410 | + "location": "Montréal", | |
| 5411 | + "notes": "beauté, lifestyle, voyage", | |
| 5412 | + "sources": [ | |
| 5413 | + "feedspot" | |
| 5414 | + ], | |
| 5415 | + "total_followers": 12700, | |
| 5416 | + "platform_count": 2, | |
| 5417 | + "tier": "micro (10K+)" | |
| 5418 | + }, | |
| 5419 | + { | |
| 5420 | + "name": "Pierrick Dubé", | |
| 5421 | + "handles": { | |
| 5422 | + "instagram": "duber72_", | |
| 5423 | + "tiktok": null, | |
| 5424 | + "youtube": null, | |
| 5425 | + "facebook": null, | |
| 5426 | + "x": null, | |
| 5427 | + "twitch": null | |
| 5428 | + }, | |
| 5429 | + "metrics": { | |
| 5430 | + "instagram_followers": 12700, | |
| 5431 | + "tiktok_followers": null, | |
| 5432 | + "youtube_subscribers": null, | |
| 5433 | + "facebook_followers": null, | |
| 5434 | + "x_followers": null, | |
| 5435 | + "twitch_followers": null | |
| 5436 | + }, | |
| 5437 | + "category": "sport", | |
| 5438 | + "location": "Québec", | |
| 5439 | + "notes": "Top 20 Modash Québec, rang 4; hockeyeur (organisation Washington Capitals); engagement 4.88%", | |
| 5440 | + "sources": [ | |
| 5441 | + "modash" | |
| 5442 | + ], | |
| 5443 | + "total_followers": 12700, | |
| 5444 | + "platform_count": 1, | |
| 5445 | + "tier": "micro (10K+)" | |
| 5446 | + }, | |
| 5447 | + { | |
| 5448 | + "name": "Kathy Brousseau", | |
| 5449 | + "handles": { | |
| 5450 | + "instagram": "kayttykat", | |
| 5451 | + "tiktok": null, | |
| 5452 | + "youtube": null, | |
| 5453 | + "facebook": null, | |
| 5454 | + "x": null, | |
| 5455 | + "twitch": null | |
| 5456 | + }, | |
| 5457 | + "metrics": { | |
| 5458 | + "instagram_followers": 12100, | |
| 5459 | + "tiktok_followers": null, | |
| 5460 | + "youtube_subscribers": null, | |
| 5461 | + "facebook_followers": null, | |
| 5462 | + "x_followers": null, | |
| 5463 | + "twitch_followers": null | |
| 5464 | + }, | |
| 5465 | + "category": "fitness", | |
| 5466 | + "location": "Québec", | |
| 5467 | + "notes": "Top 20 Modash Québec, rang 5; athlète bikini et coach fitness, infirmière; engagement 1.36%", | |
| 5468 | + "sources": [ | |
| 5469 | + "modash" | |
| 5470 | + ], | |
| 5471 | + "total_followers": 12100, | |
| 5472 | + "platform_count": 1, | |
| 5473 | + "tier": "micro (10K+)" | |
| 5474 | + }, | |
| 5475 | + { | |
| 5476 | + "name": "Eric Martel-Bahoeli", | |
| 5477 | + "handles": { | |
| 5478 | + "instagram": "bahoboxing", | |
| 5479 | + "tiktok": null, | |
| 5480 | + "youtube": null, | |
| 5481 | + "facebook": null, | |
| 5482 | + "x": null, | |
| 5483 | + "twitch": null | |
| 5484 | + }, | |
| 5485 | + "metrics": { | |
| 5486 | + "instagram_followers": 11800, | |
| 5487 | + "tiktok_followers": null, | |
| 5488 | + "youtube_subscribers": null, | |
| 5489 | + "facebook_followers": null, | |
| 5490 | + "x_followers": null, | |
| 5491 | + "twitch_followers": null | |
| 5492 | + }, | |
| 5493 | + "category": "sport", | |
| 5494 | + "location": "Québec", | |
| 5495 | + "notes": "Top 20 Modash Québec, rang 6; ex-boxeur pro, champion canadien poids lourd; engagement 1.24%", | |
| 5496 | + "sources": [ | |
| 5497 | + "modash" | |
| 5498 | + ], | |
| 5499 | + "total_followers": 11800, | |
| 5500 | + "platform_count": 1, | |
| 5501 | + "tier": "micro (10K+)" | |
| 5502 | + }, | |
| 5503 | + { | |
| 5504 | + "name": "Foodie Quebec", | |
| 5505 | + "handles": { | |
| 5506 | + "instagram": "foodiequebec", | |
| 5507 | + "tiktok": null, | |
| 5508 | + "youtube": null, | |
| 5509 | + "facebook": null, | |
| 5510 | + "x": null, | |
| 5511 | + "twitch": null | |
| 5512 | + }, | |
| 5513 | + "metrics": { | |
| 5514 | + "instagram_followers": 11800, | |
| 5515 | + "tiktok_followers": null, | |
| 5516 | + "youtube_subscribers": null, | |
| 5517 | + "facebook_followers": null, | |
| 5518 | + "x_followers": null, | |
| 5519 | + "twitch_followers": null | |
| 5520 | + }, | |
| 5521 | + "category": "food", | |
| 5522 | + "location": "Québec", | |
| 5523 | + "notes": "Sélection de découvertes culinaires au Québec (#foodieqc)", | |
| 5524 | + "sources": [ | |
| 5525 | + "feedspot_food" | |
| 5526 | + ], | |
| 5527 | + "total_followers": 11800, | |
| 5528 | + "platform_count": 1, | |
| 5529 | + "tier": "micro (10K+)" | |
| 5530 | + }, | |
| 5531 | + { | |
| 5532 | + "name": "Chanel-Océane Tardif", | |
| 5533 | + "handles": { | |
| 5534 | + "instagram": null, | |
| 5535 | + "tiktok": "chanel.octt", | |
| 5536 | + "youtube": null, | |
| 5537 | + "facebook": null, | |
| 5538 | + "x": null, | |
| 5539 | + "twitch": "chanou__" | |
| 5540 | + }, | |
| 5541 | + "metrics": { | |
| 5542 | + "instagram_followers": 2800, | |
| 5543 | + "tiktok_followers": 8500, | |
| 5544 | + "youtube_subscribers": null, | |
| 5545 | + "facebook_followers": null, | |
| 5546 | + "x_followers": null, | |
| 5547 | + "twitch_followers": 491 | |
| 5548 | + }, | |
| 5549 | + "category": "beauté", | |
| 5550 | + "location": "Québec (ville), Québec", | |
| 5551 | + "notes": "Beauté, mode, lifestyle, voyage et danse. YouTube 10k-50k, Twitch 491. | Créatrice beauté, mode, lifestyle, voyage et danse; YouTube 10k-50k (fourchette)", | |
| 5552 | + "sources": [ | |
| 5553 | + "collabstr_tiktok", | |
| 5554 | + "collabstr_twitch" | |
| 5555 | + ], | |
| 5556 | + "total_followers": 11791, | |
| 5557 | + "platform_count": 3, | |
| 5558 | + "tier": "micro (10K+)" | |
| 5559 | + }, | |
| 5560 | + { | |
| 5561 | + "name": "Phil Gagné", | |
| 5562 | + "handles": { | |
| 5563 | + "instagram": "kaikupanda", | |
| 5564 | + "tiktok": null, | |
| 5565 | + "youtube": null, | |
| 5566 | + "facebook": null, | |
| 5567 | + "x": null, | |
| 5568 | + "twitch": null | |
| 5569 | + }, | |
| 5570 | + "metrics": { | |
| 5571 | + "instagram_followers": 11600, | |
| 5572 | + "tiktok_followers": null, | |
| 5573 | + "youtube_subscribers": null, | |
| 5574 | + "facebook_followers": null, | |
| 5575 | + "x_followers": null, | |
| 5576 | + "twitch_followers": null | |
| 5577 | + }, | |
| 5578 | + "category": "sport", | |
| 5579 | + "location": "Québec", | |
| 5580 | + "notes": "Top 20 Modash Québec, rang 7; vélo (@velocartel), musique et photo; engagement 2.59%", | |
| 5581 | + "sources": [ | |
| 5582 | + "modash" | |
| 5583 | + ], | |
| 5584 | + "total_followers": 11600, | |
| 5585 | + "platform_count": 1, | |
| 5586 | + "tier": "micro (10K+)" | |
| 5587 | + }, | |
| 5588 | + { | |
| 5589 | + "name": "Yanick Turcotte", | |
| 5590 | + "handles": { | |
| 5591 | + "instagram": "yturcotte17", | |
| 5592 | + "tiktok": null, | |
| 5593 | + "youtube": null, | |
| 5594 | + "facebook": null, | |
| 5595 | + "x": null, | |
| 5596 | + "twitch": null | |
| 5597 | + }, | |
| 5598 | + "metrics": { | |
| 5599 | + "instagram_followers": 11400, | |
| 5600 | + "tiktok_followers": null, | |
| 5601 | + "youtube_subscribers": null, | |
| 5602 | + "facebook_followers": null, | |
| 5603 | + "x_followers": null, | |
| 5604 | + "twitch_followers": null | |
| 5605 | + }, | |
| 5606 | + "category": "lifestyle", | |
| 5607 | + "location": "Québec", | |
| 5608 | + "notes": "Top 20 Modash Québec, rang 8; engagement 10.17%", | |
| 5609 | + "sources": [ | |
| 5610 | + "modash" | |
| 5611 | + ], | |
| 5612 | + "total_followers": 11400, | |
| 5613 | + "platform_count": 1, | |
| 5614 | + "tier": "micro (10K+)" | |
| 5615 | + }, | |
| 5616 | + { | |
| 5617 | + "name": "Émilie Gaillet (Émilie Murmure)", | |
| 5618 | + "handles": { | |
| 5619 | + "instagram": "emilie_murmure", | |
| 5620 | + "tiktok": null, | |
| 5621 | + "youtube": null, | |
| 5622 | + "facebook": null, | |
| 5623 | + "x": null, | |
| 5624 | + "twitch": null | |
| 5625 | + }, | |
| 5626 | + "metrics": { | |
| 5627 | + "instagram_followers": 11300, | |
| 5628 | + "tiktok_followers": null, | |
| 5629 | + "youtube_subscribers": null, | |
| 5630 | + "facebook_followers": null, | |
| 5631 | + "x_followers": null, | |
| 5632 | + "twitch_followers": null | |
| 5633 | + }, | |
| 5634 | + "category": "food", | |
| 5635 | + "location": "Montréal", | |
| 5636 | + "notes": "Photographe et styliste culinaire, création de recettes; blogue primé Taste Canada Awards 2018", | |
| 5637 | + "sources": [ | |
| 5638 | + "feedspot_food", | |
| 5639 | + "clark_food" | |
| 5640 | + ], | |
| 5641 | + "total_followers": 11300, | |
| 5642 | + "platform_count": 1, | |
| 5643 | + "tier": "micro (10K+)" | |
| 5644 | + }, | |
| 5645 | + { | |
| 5646 | + "name": "Genevieve Wong", | |
| 5647 | + "handles": { | |
| 5648 | + "instagram": "goodgirlgigi_", | |
| 5649 | + "tiktok": null, | |
| 5650 | + "youtube": null, | |
| 5651 | + "facebook": null, | |
| 5652 | + "x": null, | |
| 5653 | + "twitch": null | |
| 5654 | + }, | |
| 5655 | + "metrics": { | |
| 5656 | + "instagram_followers": 11200, | |
| 5657 | + "tiktok_followers": null, | |
| 5658 | + "youtube_subscribers": null, | |
| 5659 | + "facebook_followers": null, | |
| 5660 | + "x_followers": null, | |
| 5661 | + "twitch_followers": null | |
| 5662 | + }, | |
| 5663 | + "category": "lifestyle", | |
| 5664 | + "location": "Montréal", | |
| 5665 | + "notes": "lifestyle, maternité et mode; maman de jumeaux", | |
| 5666 | + "sources": [ | |
| 5667 | + "collabstr" | |
| 5668 | + ], | |
| 5669 | + "total_followers": 11200, | |
| 5670 | + "platform_count": 1, | |
| 5671 | + "tier": "micro (10K+)" | |
| 5672 | + }, | |
| 5673 | + { | |
| 5674 | + "name": "V. (verolagachette)", | |
| 5675 | + "handles": { | |
| 5676 | + "instagram": "verolagachette", | |
| 5677 | + "tiktok": null, | |
| 5678 | + "youtube": null, | |
| 5679 | + "facebook": null, | |
| 5680 | + "x": null, | |
| 5681 | + "twitch": null | |
| 5682 | + }, | |
| 5683 | + "metrics": { | |
| 5684 | + "instagram_followers": 11200, | |
| 5685 | + "tiktok_followers": null, | |
| 5686 | + "youtube_subscribers": null, | |
| 5687 | + "facebook_followers": null, | |
| 5688 | + "x_followers": null, | |
| 5689 | + "twitch_followers": null | |
| 5690 | + }, | |
| 5691 | + "category": "autre", | |
| 5692 | + "location": "Québec", | |
| 5693 | + "notes": "Top 20 Modash Québec, rang 9; tatoueuse retraitée (@blackxlfc); engagement 1.46%", | |
| 5694 | + "sources": [ | |
| 5695 | + "modash" | |
| 5696 | + ], | |
| 5697 | + "total_followers": 11200, | |
| 5698 | + "platform_count": 1, | |
| 5699 | + "tier": "micro (10K+)" | |
| 5700 | + }, | |
| 5701 | + { | |
| 5702 | + "name": "Émilie Brun-Cormier", | |
| 5703 | + "handles": { | |
| 5704 | + "instagram": "emiliebruncormier", | |
| 5705 | + "tiktok": "emiliebruncormier", | |
| 5706 | + "youtube": null, | |
| 5707 | + "facebook": null, | |
| 5708 | + "x": null, | |
| 5709 | + "twitch": null | |
| 5710 | + }, | |
| 5711 | + "metrics": { | |
| 5712 | + "instagram_followers": 10900, | |
| 5713 | + "tiktok_followers": null, | |
| 5714 | + "youtube_subscribers": null, | |
| 5715 | + "facebook_followers": null, | |
| 5716 | + "x_followers": null, | |
| 5717 | + "twitch_followers": null | |
| 5718 | + }, | |
| 5719 | + "category": "lifestyle", | |
| 5720 | + "location": "Québec", | |
| 5721 | + "notes": "Top 20 Modash Québec, rang 10; plein air, ambassadrice Sports Experts Laurier; engagement 3.9%", | |
| 5722 | + "sources": [ | |
| 5723 | + "modash" | |
| 5724 | + ], | |
| 5725 | + "total_followers": 10900, | |
| 5726 | + "platform_count": 2, | |
| 5727 | + "tier": "micro (10K+)" | |
| 5728 | + }, | |
| 5729 | + { | |
| 5730 | + "name": "Lauréan Tardif", | |
| 5731 | + "handles": { | |
| 5732 | + "instagram": "laureantardif", | |
| 5733 | + "tiktok": null, | |
| 5734 | + "youtube": null, | |
| 5735 | + "facebook": null, | |
| 5736 | + "x": null, | |
| 5737 | + "twitch": null | |
| 5738 | + }, | |
| 5739 | + "metrics": { | |
| 5740 | + "instagram_followers": 10900, | |
| 5741 | + "tiktok_followers": null, | |
| 5742 | + "youtube_subscribers": null, | |
| 5743 | + "facebook_followers": null, | |
| 5744 | + "x_followers": null, | |
| 5745 | + "twitch_followers": null | |
| 5746 | + }, | |
| 5747 | + "category": "voyage", | |
| 5748 | + "location": "Québec", | |
| 5749 | + "notes": "Top 20 Modash Québec, rang 11; vidéaste d'expédition / plein air; engagement 2.8%", | |
| 5750 | + "sources": [ | |
| 5751 | + "modash" | |
| 5752 | + ], | |
| 5753 | + "total_followers": 10900, | |
| 5754 | + "platform_count": 1, | |
| 5755 | + "tier": "micro (10K+)" | |
| 5756 | + }, | |
| 5757 | + { | |
| 5758 | + "name": "École de cirque de Québec", | |
| 5759 | + "handles": { | |
| 5760 | + "instagram": "ecoledecirquedequebec", | |
| 5761 | + "tiktok": null, | |
| 5762 | + "youtube": null, | |
| 5763 | + "facebook": null, | |
| 5764 | + "x": null, | |
| 5765 | + "twitch": null | |
| 5766 | + }, | |
| 5767 | + "metrics": { | |
| 5768 | + "instagram_followers": 10800, | |
| 5769 | + "tiktok_followers": null, | |
| 5770 | + "youtube_subscribers": null, | |
| 5771 | + "facebook_followers": null, | |
| 5772 | + "x_followers": null, | |
| 5773 | + "twitch_followers": null | |
| 5774 | + }, | |
| 5775 | + "category": "autre", | |
| 5776 | + "location": "Québec", | |
| 5777 | + "notes": "Top 20 Modash Québec, rang 12; école de cirque contemporain (organisation); engagement 1.41%", | |
| 5778 | + "sources": [ | |
| 5779 | + "modash" | |
| 5780 | + ], | |
| 5781 | + "total_followers": 10800, | |
| 5782 | + "platform_count": 1, | |
| 5783 | + "tier": "micro (10K+)" | |
| 5784 | + }, | |
| 5785 | + { | |
| 5786 | + "name": "Olivier Lohman", | |
| 5787 | + "handles": { | |
| 5788 | + "instagram": "speedbird3000", | |
| 5789 | + "tiktok": null, | |
| 5790 | + "youtube": null, | |
| 5791 | + "facebook": null, | |
| 5792 | + "x": null, | |
| 5793 | + "twitch": null | |
| 5794 | + }, | |
| 5795 | + "metrics": { | |
| 5796 | + "instagram_followers": 10600, | |
| 5797 | + "tiktok_followers": null, | |
| 5798 | + "youtube_subscribers": null, | |
| 5799 | + "facebook_followers": null, | |
| 5800 | + "x_followers": null, | |
| 5801 | + "twitch_followers": null | |
| 5802 | + }, | |
| 5803 | + "category": "autre", | |
| 5804 | + "location": "Québec", | |
| 5805 | + "notes": "Top 20 Modash Québec, rang 13; contenu aviation / pilote; engagement 10.96%", | |
| 5806 | + "sources": [ | |
| 5807 | + "modash" | |
| 5808 | + ], | |
| 5809 | + "total_followers": 10600, | |
| 5810 | + "platform_count": 1, | |
| 5811 | + "tier": "micro (10K+)" | |
| 5812 | + }, | |
| 5813 | + { | |
| 5814 | + "name": "Katia Curadeau", | |
| 5815 | + "handles": { | |
| 5816 | + "instagram": "katiacuradeau", | |
| 5817 | + "tiktok": null, | |
| 5818 | + "youtube": null, | |
| 5819 | + "facebook": null, | |
| 5820 | + "x": null, | |
| 5821 | + "twitch": null | |
| 5822 | + }, | |
| 5823 | + "metrics": { | |
| 5824 | + "instagram_followers": 10500, | |
| 5825 | + "tiktok_followers": null, | |
| 5826 | + "youtube_subscribers": null, | |
| 5827 | + "facebook_followers": null, | |
| 5828 | + "x_followers": null, | |
| 5829 | + "twitch_followers": null | |
| 5830 | + }, | |
| 5831 | + "category": "mode", | |
| 5832 | + "location": "Québec", | |
| 5833 | + "notes": "Top 20 Modash Québec, rang 14; modèle photo / photographie (@photokurado); engagement 1.65%", | |
| 5834 | + "sources": [ | |
| 5835 | + "modash" | |
| 5836 | + ], | |
| 5837 | + "total_followers": 10500, | |
| 5838 | + "platform_count": 1, | |
| 5839 | + "tier": "micro (10K+)" | |
| 5840 | + }, | |
| 5841 | + { | |
| 5842 | + "name": "Kristeena Saint", | |
| 5843 | + "handles": { | |
| 5844 | + "instagram": "kristeenamichelle", | |
| 5845 | + "tiktok": null, | |
| 5846 | + "youtube": null, | |
| 5847 | + "facebook": null, | |
| 5848 | + "x": null, | |
| 5849 | + "twitch": null | |
| 5850 | + }, | |
| 5851 | + "metrics": { | |
| 5852 | + "instagram_followers": 10400, | |
| 5853 | + "tiktok_followers": null, | |
| 5854 | + "youtube_subscribers": null, | |
| 5855 | + "facebook_followers": null, | |
| 5856 | + "x_followers": null, | |
| 5857 | + "twitch_followers": null | |
| 5858 | + }, | |
| 5859 | + "category": "food", | |
| 5860 | + "location": "Montréal", | |
| 5861 | + "notes": "Foodie pigiste", | |
| 5862 | + "sources": [ | |
| 5863 | + "feedspot_food" | |
| 5864 | + ], | |
| 5865 | + "total_followers": 10400, | |
| 5866 | + "platform_count": 1, | |
| 5867 | + "tier": "micro (10K+)" | |
| 5868 | + }, | |
| 5869 | + { | |
| 5870 | + "name": "Annabelle Gervais-Tanguay (anna)", | |
| 5871 | + "handles": { | |
| 5872 | + "instagram": "annagervaiss", | |
| 5873 | + "tiktok": null, | |
| 5874 | + "youtube": null, | |
| 5875 | + "facebook": null, | |
| 5876 | + "x": null, | |
| 5877 | + "twitch": null | |
| 5878 | + }, | |
| 5879 | + "metrics": { | |
| 5880 | + "instagram_followers": 10100, | |
| 5881 | + "tiktok_followers": null, | |
| 5882 | + "youtube_subscribers": null, | |
| 5883 | + "facebook_followers": null, | |
| 5884 | + "x_followers": null, | |
| 5885 | + "twitch_followers": null | |
| 5886 | + }, | |
| 5887 | + "category": "lifestyle", | |
| 5888 | + "location": "Québec", | |
| 5889 | + "notes": "Top 20 Modash Québec, rang 15; engagement 9.12%", | |
| 5890 | + "sources": [ | |
| 5891 | + "modash" | |
| 5892 | + ], | |
| 5893 | + "total_followers": 10100, | |
| 5894 | + "platform_count": 1, | |
| 5895 | + "tier": "micro (10K+)" | |
| 5896 | + }, | |
| 5897 | + { | |
| 5898 | + "name": "Cynthia (cl_blog)", | |
| 5899 | + "handles": { | |
| 5900 | + "instagram": "cl_blog", | |
| 5901 | + "tiktok": null, | |
| 5902 | + "youtube": null, | |
| 5903 | + "facebook": null, | |
| 5904 | + "x": null, | |
| 5905 | + "twitch": null | |
| 5906 | + }, | |
| 5907 | + "metrics": { | |
| 5908 | + "instagram_followers": 10100, | |
| 5909 | + "tiktok_followers": null, | |
| 5910 | + "youtube_subscribers": null, | |
| 5911 | + "facebook_followers": null, | |
| 5912 | + "x_followers": null, | |
| 5913 | + "twitch_followers": null | |
| 5914 | + }, | |
| 5915 | + "category": "food", | |
| 5916 | + "location": "Montréal", | |
| 5917 | + "notes": "Blogueuse MTL: food, lifestyle, activités, voyage | Aventures culinaires et contenu UGC", | |
| 5918 | + "sources": [ | |
| 5919 | + "feedspot_food" | |
| 5920 | + ], | |
| 5921 | + "total_followers": 10100, | |
| 5922 | + "platform_count": 1, | |
| 5923 | + "tier": "micro (10K+)" | |
| 5924 | + }, | |
| 5925 | + { | |
| 5926 | + "name": "Girlie Owusu", | |
| 5927 | + "handles": { | |
| 5928 | + "instagram": "blackasiian", | |
| 5929 | + "tiktok": null, | |
| 5930 | + "youtube": null, | |
| 5931 | + "facebook": null, | |
| 5932 | + "x": null, | |
| 5933 | + "twitch": null | |
| 5934 | + }, | |
| 5935 | + "metrics": { | |
| 5936 | + "instagram_followers": 9900, | |
| 5937 | + "tiktok_followers": null, | |
| 5938 | + "youtube_subscribers": null, | |
| 5939 | + "facebook_followers": null, | |
| 5940 | + "x_followers": null, | |
| 5941 | + "twitch_followers": null | |
| 5942 | + }, | |
| 5943 | + "category": "mode", | |
| 5944 | + "location": "Montréal", | |
| 5945 | + "notes": "mode, cheveux bouclés, beauté et skincare; mannequin", | |
| 5946 | + "sources": [ | |
| 5947 | + "collabstr" | |
| 5948 | + ], | |
| 5949 | + "total_followers": 9900, | |
| 5950 | + "platform_count": 1, | |
| 5951 | + "tier": "nano/inconnu" | |
| 5952 | + }, | |
| 5953 | + { | |
| 5954 | + "name": "Eben Gomat", | |
| 5955 | + "handles": { | |
| 5956 | + "instagram": "gomateben", | |
| 5957 | + "tiktok": null, | |
| 5958 | + "youtube": null, | |
| 5959 | + "facebook": null, | |
| 5960 | + "x": null, | |
| 5961 | + "twitch": null | |
| 5962 | + }, | |
| 5963 | + "metrics": { | |
| 5964 | + "instagram_followers": 9800, | |
| 5965 | + "tiktok_followers": null, | |
| 5966 | + "youtube_subscribers": null, | |
| 5967 | + "facebook_followers": null, | |
| 5968 | + "x_followers": null, | |
| 5969 | + "twitch_followers": null | |
| 5970 | + }, | |
| 5971 | + "category": "fitness", | |
| 5972 | + "location": "Montréal", | |
| 5973 | + "notes": "santé, fitness et mode masculine", | |
| 5974 | + "sources": [ | |
| 5975 | + "feedspot" | |
| 5976 | + ], | |
| 5977 | + "total_followers": 9800, | |
| 5978 | + "platform_count": 1, | |
| 5979 | + "tier": "nano/inconnu" | |
| 5980 | + }, | |
| 5981 | + { | |
| 5982 | + "name": "Lisa Cote", | |
| 5983 | + "handles": { | |
| 5984 | + "instagram": "liisacotee__", | |
| 5985 | + "tiktok": null, | |
| 5986 | + "youtube": null, | |
| 5987 | + "facebook": null, | |
| 5988 | + "x": null, | |
| 5989 | + "twitch": null | |
| 5990 | + }, | |
| 5991 | + "metrics": { | |
| 5992 | + "instagram_followers": 9500, | |
| 5993 | + "tiktok_followers": null, | |
| 5994 | + "youtube_subscribers": null, | |
| 5995 | + "facebook_followers": null, | |
| 5996 | + "x_followers": null, | |
| 5997 | + "twitch_followers": null | |
| 5998 | + }, | |
| 5999 | + "category": "autre", | |
| 6000 | + "location": "Québec", | |
| 6001 | + "notes": "UGC et photographie de produit", | |
| 6002 | + "sources": [ | |
| 6003 | + "collabstr" | |
| 6004 | + ], | |
| 6005 | + "total_followers": 9500, | |
| 6006 | + "platform_count": 1, | |
| 6007 | + "tier": "nano/inconnu" | |
| 6008 | + }, | |
| 6009 | + { | |
| 6010 | + "name": "Kelly Ann Turcot", | |
| 6011 | + "handles": { | |
| 6012 | + "instagram": "kellyannturcot", | |
| 6013 | + "tiktok": null, | |
| 6014 | + "youtube": null, | |
| 6015 | + "facebook": null, | |
| 6016 | + "x": null, | |
| 6017 | + "twitch": null | |
| 6018 | + }, | |
| 6019 | + "metrics": { | |
| 6020 | + "instagram_followers": 9400, | |
| 6021 | + "tiktok_followers": null, | |
| 6022 | + "youtube_subscribers": null, | |
| 6023 | + "facebook_followers": null, | |
| 6024 | + "x_followers": null, | |
| 6025 | + "twitch_followers": null | |
| 6026 | + }, | |
| 6027 | + "category": "lifestyle", | |
| 6028 | + "location": "Québec", | |
| 6029 | + "notes": "Top 20 Modash Québec, rang 16; partenariat @undz; engagement 6%", | |
| 6030 | + "sources": [ | |
| 6031 | + "modash" | |
| 6032 | + ], | |
| 6033 | + "total_followers": 9400, | |
| 6034 | + "platform_count": 1, | |
| 6035 | + "tier": "nano/inconnu" | |
| 6036 | + }, | |
| 6037 | + { | |
| 6038 | + "name": "Tracy Vee", | |
| 6039 | + "handles": { | |
| 6040 | + "instagram": "misstracyvee", | |
| 6041 | + "tiktok": null, | |
| 6042 | + "youtube": null, | |
| 6043 | + "facebook": null, | |
| 6044 | + "x": null, | |
| 6045 | + "twitch": null | |
| 6046 | + }, | |
| 6047 | + "metrics": { | |
| 6048 | + "instagram_followers": 9300, | |
| 6049 | + "tiktok_followers": null, | |
| 6050 | + "youtube_subscribers": null, | |
| 6051 | + "facebook_followers": null, | |
| 6052 | + "x_followers": null, | |
| 6053 | + "twitch_followers": null | |
| 6054 | + }, | |
| 6055 | + "category": "beauté", | |
| 6056 | + "location": "Montréal", | |
| 6057 | + "notes": "maquilleuse (MUA) pour cinéma, télé et pubs (@tracyveemakeup)", | |
| 6058 | + "sources": [ | |
| 6059 | + "feedspot" | |
| 6060 | + ], | |
| 6061 | + "total_followers": 9300, | |
| 6062 | + "platform_count": 1, | |
| 6063 | + "tier": "nano/inconnu" | |
| 6064 | + }, | |
| 6065 | + { | |
| 6066 | + "name": "Laurence L. Lepage", | |
| 6067 | + "handles": { | |
| 6068 | + "instagram": "laurence_l._lepage", | |
| 6069 | + "tiktok": null, | |
| 6070 | + "youtube": null, | |
| 6071 | + "facebook": null, | |
| 6072 | + "x": null, | |
| 6073 | + "twitch": null | |
| 6074 | + }, | |
| 6075 | + "metrics": { | |
| 6076 | + "instagram_followers": 9216, | |
| 6077 | + "tiktok_followers": null, | |
| 6078 | + "youtube_subscribers": null, | |
| 6079 | + "facebook_followers": null, | |
| 6080 | + "x_followers": null, | |
| 6081 | + "twitch_followers": null | |
| 6082 | + }, | |
| 6083 | + "category": "voyage", | |
| 6084 | + "location": "Québec", | |
| 6085 | + "notes": "Photographe de paysages et grands espaces, couchers de soleil", | |
| 6086 | + "sources": [ | |
| 6087 | + "noovomoi_voyage" | |
| 6088 | + ], | |
| 6089 | + "total_followers": 9216, | |
| 6090 | + "platform_count": 1, | |
| 6091 | + "tier": "nano/inconnu" | |
| 6092 | + }, | |
| 6093 | + { | |
| 6094 | + "name": "Fernanda Gonzalez", | |
| 6095 | + "handles": { | |
| 6096 | + "instagram": null, | |
| 6097 | + "tiktok": "ferchi0802", | |
| 6098 | + "youtube": null, | |
| 6099 | + "facebook": null, | |
| 6100 | + "x": null, | |
| 6101 | + "twitch": null | |
| 6102 | + }, | |
| 6103 | + "metrics": { | |
| 6104 | + "instagram_followers": null, | |
| 6105 | + "tiktok_followers": 8700, | |
| 6106 | + "youtube_subscribers": null, | |
| 6107 | + "facebook_followers": null, | |
| 6108 | + "x_followers": null, | |
| 6109 | + "twitch_followers": null | |
| 6110 | + }, | |
| 6111 | + "category": "lifestyle", | |
| 6112 | + "location": "Québec", | |
| 6113 | + "notes": "Créatrice latina en espagnol pour la communauté latino-américaine au Canada. Taux d'engagement 73.35%.", | |
| 6114 | + "sources": [ | |
| 6115 | + "collabstr_tiktok" | |
| 6116 | + ], | |
| 6117 | + "total_followers": 8700, | |
| 6118 | + "platform_count": 1, | |
| 6119 | + "tier": "nano/inconnu" | |
| 6120 | + }, | |
| 6121 | + { | |
| 6122 | + "name": "Juliette Painchaud", | |
| 6123 | + "handles": { | |
| 6124 | + "instagram": "juliette_painchaud", | |
| 6125 | + "tiktok": "juliettepainchaud", | |
| 6126 | + "youtube": null, | |
| 6127 | + "facebook": null, | |
| 6128 | + "x": null, | |
| 6129 | + "twitch": null | |
| 6130 | + }, | |
| 6131 | + "metrics": { | |
| 6132 | + "instagram_followers": 8700, | |
| 6133 | + "tiktok_followers": null, | |
| 6134 | + "youtube_subscribers": null, | |
| 6135 | + "facebook_followers": null, | |
| 6136 | + "x_followers": null, | |
| 6137 | + "twitch_followers": null | |
| 6138 | + }, | |
| 6139 | + "category": "sport", | |
| 6140 | + "location": "Québec", | |
| 6141 | + "notes": "Top 20 Modash Québec, rang 17; marathonienne (Boston Marathon); engagement 9.16%", | |
| 6142 | + "sources": [ | |
| 6143 | + "modash" | |
| 6144 | + ], | |
| 6145 | + "total_followers": 8700, | |
| 6146 | + "platform_count": 2, | |
| 6147 | + "tier": "nano/inconnu" | |
| 6148 | + }, | |
| 6149 | + { | |
| 6150 | + "name": "Benoit Boulanger", | |
| 6151 | + "handles": { | |
| 6152 | + "instagram": "ben.boulanger", | |
| 6153 | + "tiktok": null, | |
| 6154 | + "youtube": null, | |
| 6155 | + "facebook": null, | |
| 6156 | + "x": null, | |
| 6157 | + "twitch": null | |
| 6158 | + }, | |
| 6159 | + "metrics": { | |
| 6160 | + "instagram_followers": 8600, | |
| 6161 | + "tiktok_followers": null, | |
| 6162 | + "youtube_subscribers": null, | |
| 6163 | + "facebook_followers": null, | |
| 6164 | + "x_followers": null, | |
| 6165 | + "twitch_followers": null | |
| 6166 | + }, | |
| 6167 | + "category": "fitness", | |
| 6168 | + "location": "Québec", | |
| 6169 | + "notes": "Top 20 Modash Québec, rang 18; athlète CrossFit et nutritionniste Dt.P; engagement 3.52%", | |
| 6170 | + "sources": [ | |
| 6171 | + "modash" | |
| 6172 | + ], | |
| 6173 | + "total_followers": 8600, | |
| 6174 | + "platform_count": 1, | |
| 6175 | + "tier": "nano/inconnu" | |
| 6176 | + }, | |
| 6177 | + { | |
| 6178 | + "name": "Abi Rodier", | |
| 6179 | + "handles": { | |
| 6180 | + "instagram": "aaabirodier", | |
| 6181 | + "tiktok": null, | |
| 6182 | + "youtube": null, | |
| 6183 | + "facebook": null, | |
| 6184 | + "x": null, | |
| 6185 | + "twitch": null | |
| 6186 | + }, | |
| 6187 | + "metrics": { | |
| 6188 | + "instagram_followers": 8500, | |
| 6189 | + "tiktok_followers": null, | |
| 6190 | + "youtube_subscribers": null, | |
| 6191 | + "facebook_followers": null, | |
| 6192 | + "x_followers": null, | |
| 6193 | + "twitch_followers": null | |
| 6194 | + }, | |
| 6195 | + "category": "lifestyle", | |
| 6196 | + "location": "Québec", | |
| 6197 | + "notes": "Top 20 Modash Québec, rang 19; engagement 17.59%", | |
| 6198 | + "sources": [ | |
| 6199 | + "modash" | |
| 6200 | + ], | |
| 6201 | + "total_followers": 8500, | |
| 6202 | + "platform_count": 1, | |
| 6203 | + "tier": "nano/inconnu" | |
| 6204 | + }, | |
| 6205 | + { | |
| 6206 | + "name": "Charles Nguyen", | |
| 6207 | + "handles": { | |
| 6208 | + "instagram": "mtlcharles", | |
| 6209 | + "tiktok": null, | |
| 6210 | + "youtube": null, | |
| 6211 | + "facebook": null, | |
| 6212 | + "x": null, | |
| 6213 | + "twitch": null | |
| 6214 | + }, | |
| 6215 | + "metrics": { | |
| 6216 | + "instagram_followers": 8400, | |
| 6217 | + "tiktok_followers": null, | |
| 6218 | + "youtube_subscribers": null, | |
| 6219 | + "facebook_followers": null, | |
| 6220 | + "x_followers": null, | |
| 6221 | + "twitch_followers": null | |
| 6222 | + }, | |
| 6223 | + "category": "food", | |
| 6224 | + "location": "Montréal", | |
| 6225 | + "notes": "Créateur, photographe, auteur; culture vietnamienne (@chodemmtl)", | |
| 6226 | + "sources": [ | |
| 6227 | + "feedspot_food" | |
| 6228 | + ], | |
| 6229 | + "total_followers": 8400, | |
| 6230 | + "platform_count": 1, | |
| 6231 | + "tier": "nano/inconnu" | |
| 6232 | + }, | |
| 6233 | + { | |
| 6234 | + "name": "Claudia Passalacqua", | |
| 6235 | + "handles": { | |
| 6236 | + "instagram": "claudiapassa", | |
| 6237 | + "tiktok": null, | |
| 6238 | + "youtube": null, | |
| 6239 | + "facebook": null, | |
| 6240 | + "x": null, | |
| 6241 | + "twitch": null | |
| 6242 | + }, | |
| 6243 | + "metrics": { | |
| 6244 | + "instagram_followers": 8400, | |
| 6245 | + "tiktok_followers": null, | |
| 6246 | + "youtube_subscribers": null, | |
| 6247 | + "facebook_followers": null, | |
| 6248 | + "x_followers": null, | |
| 6249 | + "twitch_followers": null | |
| 6250 | + }, | |
| 6251 | + "category": "food", | |
| 6252 | + "location": "Montréal", | |
| 6253 | + "notes": "Stratège de contenu et gestionnaire de réseaux sociaux", | |
| 6254 | + "sources": [ | |
| 6255 | + "feedspot_food" | |
| 6256 | + ], | |
| 6257 | + "total_followers": 8400, | |
| 6258 | + "platform_count": 1, | |
| 6259 | + "tier": "nano/inconnu" | |
| 6260 | + }, | |
| 6261 | + { | |
| 6262 | + "name": "Géraldine Drolet", | |
| 6263 | + "handles": { | |
| 6264 | + "instagram": "geraldinedrolet", | |
| 6265 | + "tiktok": null, | |
| 6266 | + "youtube": null, | |
| 6267 | + "facebook": null, | |
| 6268 | + "x": null, | |
| 6269 | + "twitch": null | |
| 6270 | + }, | |
| 6271 | + "metrics": { | |
| 6272 | + "instagram_followers": 8200, | |
| 6273 | + "tiktok_followers": null, | |
| 6274 | + "youtube_subscribers": null, | |
| 6275 | + "facebook_followers": null, | |
| 6276 | + "x_followers": null, | |
| 6277 | + "twitch_followers": null | |
| 6278 | + }, | |
| 6279 | + "category": "lifestyle", | |
| 6280 | + "location": "Québec", | |
| 6281 | + "notes": "Top 20 Modash Québec, rang 20; collab Red Bull Canada; engagement 1.23%", | |
| 6282 | + "sources": [ | |
| 6283 | + "modash" | |
| 6284 | + ], | |
| 6285 | + "total_followers": 8200, | |
| 6286 | + "platform_count": 1, | |
| 6287 | + "tier": "nano/inconnu" | |
| 6288 | + }, | |
| 6289 | + { | |
| 6290 | + "name": "Jonika (Xx.Jojo_Ac)", | |
| 6291 | + "handles": { | |
| 6292 | + "instagram": "xx.jojo_ac", | |
| 6293 | + "tiktok": null, | |
| 6294 | + "youtube": null, | |
| 6295 | + "facebook": null, | |
| 6296 | + "x": null, | |
| 6297 | + "twitch": null | |
| 6298 | + }, | |
| 6299 | + "metrics": { | |
| 6300 | + "instagram_followers": 5500, | |
| 6301 | + "tiktok_followers": 2400, | |
| 6302 | + "youtube_subscribers": null, | |
| 6303 | + "facebook_followers": null, | |
| 6304 | + "x_followers": null, | |
| 6305 | + "twitch_followers": null | |
| 6306 | + }, | |
| 6307 | + "category": "beauté", | |
| 6308 | + "location": "Montréal", | |
| 6309 | + "notes": "beauté et lifestyle, UGC; collabs Hoaka Swimwear entre autres", | |
| 6310 | + "sources": [ | |
| 6311 | + "collabstr" | |
| 6312 | + ], | |
| 6313 | + "total_followers": 7900, | |
| 6314 | + "platform_count": 2, | |
| 6315 | + "tier": "nano/inconnu" | |
| 6316 | + }, | |
| 6317 | + { | |
| 6318 | + "name": "Philippe Marquis", | |
| 6319 | + "handles": { | |
| 6320 | + "instagram": "philmarquis", | |
| 6321 | + "tiktok": null, | |
| 6322 | + "youtube": null, | |
| 6323 | + "facebook": null, | |
| 6324 | + "x": null, | |
| 6325 | + "twitch": null | |
| 6326 | + }, | |
| 6327 | + "metrics": { | |
| 6328 | + "instagram_followers": 7800, | |
| 6329 | + "tiktok_followers": null, | |
| 6330 | + "youtube_subscribers": null, | |
| 6331 | + "facebook_followers": null, | |
| 6332 | + "x_followers": null, | |
| 6333 | + "twitch_followers": null | |
| 6334 | + }, | |
| 6335 | + "category": "autre", | |
| 6336 | + "location": "Québec", | |
| 6337 | + "notes": "olympien en ski acrobatique (Sochi 2014, PyeongChang 2018), défenseur du climat", | |
| 6338 | + "sources": [ | |
| 6339 | + "feedspot" | |
| 6340 | + ], | |
| 6341 | + "total_followers": 7800, | |
| 6342 | + "platform_count": 1, | |
| 6343 | + "tier": "nano/inconnu" | |
| 6344 | + }, | |
| 6345 | + { | |
| 6346 | + "name": "Priyanka (pri_mtlfoodie)", | |
| 6347 | + "handles": { | |
| 6348 | + "instagram": "pri_mtlfoodie", | |
| 6349 | + "tiktok": null, | |
| 6350 | + "youtube": null, | |
| 6351 | + "facebook": null, | |
| 6352 | + "x": null, | |
| 6353 | + "twitch": null | |
| 6354 | + }, | |
| 6355 | + "metrics": { | |
| 6356 | + "instagram_followers": 7600, | |
| 6357 | + "tiktok_followers": null, | |
| 6358 | + "youtube_subscribers": null, | |
| 6359 | + "facebook_followers": null, | |
| 6360 | + "x_followers": null, | |
| 6361 | + "twitch_followers": null | |
| 6362 | + }, | |
| 6363 | + "category": "food", | |
| 6364 | + "location": "Montréal", | |
| 6365 | + "notes": "Food, activités et voyage EN/FR", | |
| 6366 | + "sources": [ | |
| 6367 | + "feedspot_food" | |
| 6368 | + ], | |
| 6369 | + "total_followers": 7600, | |
| 6370 | + "platform_count": 1, | |
| 6371 | + "tier": "nano/inconnu" | |
| 6372 | + }, | |
| 6373 | + { | |
| 6374 | + "name": "Amy Fraser", | |
| 6375 | + "handles": { | |
| 6376 | + "instagram": "amyfraserxox", | |
| 6377 | + "tiktok": null, | |
| 6378 | + "youtube": null, | |
| 6379 | + "facebook": null, | |
| 6380 | + "x": null, | |
| 6381 | + "twitch": null | |
| 6382 | + }, | |
| 6383 | + "metrics": { | |
| 6384 | + "instagram_followers": 7500, | |
| 6385 | + "tiktok_followers": null, | |
| 6386 | + "youtube_subscribers": null, | |
| 6387 | + "facebook_followers": null, | |
| 6388 | + "x_followers": null, | |
| 6389 | + "twitch_followers": null | |
| 6390 | + }, | |
| 6391 | + "category": "beauté", | |
| 6392 | + "location": "Montréal", | |
| 6393 | + "notes": "beauté, mode et lifestyle; engagement 8.79%; Top Creator Collabstr", | |
| 6394 | + "sources": [ | |
| 6395 | + "collabstr" | |
| 6396 | + ], | |
| 6397 | + "total_followers": 7500, | |
| 6398 | + "platform_count": 1, | |
| 6399 | + "tier": "nano/inconnu" | |
| 6400 | + }, | |
| 6401 | + { | |
| 6402 | + "name": "Béatrice Bernard-Poulin", | |
| 6403 | + "handles": { | |
| 6404 | + "instagram": "beatricebp", | |
| 6405 | + "tiktok": null, | |
| 6406 | + "youtube": null, | |
| 6407 | + "facebook": null, | |
| 6408 | + "x": null, | |
| 6409 | + "twitch": null | |
| 6410 | + }, | |
| 6411 | + "metrics": { | |
| 6412 | + "instagram_followers": 7000, | |
| 6413 | + "tiktok_followers": null, | |
| 6414 | + "youtube_subscribers": null, | |
| 6415 | + "facebook_followers": null, | |
| 6416 | + "x_followers": null, | |
| 6417 | + "twitch_followers": null | |
| 6418 | + }, | |
| 6419 | + "category": "voyage", | |
| 6420 | + "location": "Québec", | |
| 6421 | + "notes": "Photos de voyages, nourriture et cocktails", | |
| 6422 | + "sources": [ | |
| 6423 | + "noovomoi_voyage" | |
| 6424 | + ], | |
| 6425 | + "total_followers": 7000, | |
| 6426 | + "platform_count": 1, | |
| 6427 | + "tier": "nano/inconnu" | |
| 6428 | + }, | |
| 6429 | + { | |
| 6430 | + "name": "Alexa Flynn", | |
| 6431 | + "handles": { | |
| 6432 | + "instagram": "shlex", | |
| 6433 | + "tiktok": null, | |
| 6434 | + "youtube": null, | |
| 6435 | + "facebook": null, | |
| 6436 | + "x": null, | |
| 6437 | + "twitch": null | |
| 6438 | + }, | |
| 6439 | + "metrics": { | |
| 6440 | + "instagram_followers": 6500, | |
| 6441 | + "tiktok_followers": null, | |
| 6442 | + "youtube_subscribers": null, | |
| 6443 | + "facebook_followers": null, | |
| 6444 | + "x_followers": null, | |
| 6445 | + "twitch_followers": null | |
| 6446 | + }, | |
| 6447 | + "category": "fitness", | |
| 6448 | + "location": "Montréal", | |
| 6449 | + "notes": "prof de Pilates et entraîneuse personnelle, cofondatrice @tonebyshab", | |
| 6450 | + "sources": [ | |
| 6451 | + "feedspot" | |
| 6452 | + ], | |
| 6453 | + "total_followers": 6500, | |
| 6454 | + "platform_count": 1, | |
| 6455 | + "tier": "nano/inconnu" | |
| 6456 | + }, | |
| 6457 | + { | |
| 6458 | + "name": "Marion", | |
| 6459 | + "handles": { | |
| 6460 | + "instagram": "marion.csrd", | |
| 6461 | + "tiktok": null, | |
| 6462 | + "youtube": null, | |
| 6463 | + "facebook": null, | |
| 6464 | + "x": null, | |
| 6465 | + "twitch": null | |
| 6466 | + }, | |
| 6467 | + "metrics": { | |
| 6468 | + "instagram_followers": 6400, | |
| 6469 | + "tiktok_followers": null, | |
| 6470 | + "youtube_subscribers": null, | |
| 6471 | + "facebook_followers": null, | |
| 6472 | + "x_followers": null, | |
| 6473 | + "twitch_followers": null | |
| 6474 | + }, | |
| 6475 | + "category": "lifestyle", | |
| 6476 | + "location": "Montréal", | |
| 6477 | + "notes": "yoga et matcha, Montréal/Paris", | |
| 6478 | + "sources": [ | |
| 6479 | + "feedspot" | |
| 6480 | + ], | |
| 6481 | + "total_followers": 6400, | |
| 6482 | + "platform_count": 1, | |
| 6483 | + "tier": "nano/inconnu" | |
| 6484 | + }, | |
| 6485 | + { | |
| 6486 | + "name": "Himani Jadeja", | |
| 6487 | + "handles": { | |
| 6488 | + "instagram": "himani_jadeja", | |
| 6489 | + "tiktok": null, | |
| 6490 | + "youtube": null, | |
| 6491 | + "facebook": null, | |
| 6492 | + "x": null, | |
| 6493 | + "twitch": null | |
| 6494 | + }, | |
| 6495 | + "metrics": { | |
| 6496 | + "instagram_followers": 6200, | |
| 6497 | + "tiktok_followers": null, | |
| 6498 | + "youtube_subscribers": null, | |
| 6499 | + "facebook_followers": null, | |
| 6500 | + "x_followers": null, | |
| 6501 | + "twitch_followers": null | |
| 6502 | + }, | |
| 6503 | + "category": "mode", | |
| 6504 | + "location": "Montréal", | |
| 6505 | + "notes": "designer de mode et styliste (Canada/Koweït/Inde)", | |
| 6506 | + "sources": [ | |
| 6507 | + "feedspot" | |
| 6508 | + ], | |
| 6509 | + "total_followers": 6200, | |
| 6510 | + "platform_count": 1, | |
| 6511 | + "tier": "nano/inconnu" | |
| 6512 | + }, | |
| 6513 | + { | |
| 6514 | + "name": "Maude Robichaud Godin", | |
| 6515 | + "handles": { | |
| 6516 | + "instagram": null, | |
| 6517 | + "tiktok": "smiti.maude", | |
| 6518 | + "youtube": null, | |
| 6519 | + "facebook": null, | |
| 6520 | + "x": null, | |
| 6521 | + "twitch": null | |
| 6522 | + }, | |
| 6523 | + "metrics": { | |
| 6524 | + "instagram_followers": 3700, | |
| 6525 | + "tiktok_followers": 1900, | |
| 6526 | + "youtube_subscribers": null, | |
| 6527 | + "facebook_followers": null, | |
| 6528 | + "x_followers": null, | |
| 6529 | + "twitch_followers": null | |
| 6530 | + }, | |
| 6531 | + "category": "food", | |
| 6532 | + "location": "Québec", | |
| 6533 | + "notes": "Cuisine et culture marocaines, apprentissage du darija, vie de couple.", | |
| 6534 | + "sources": [ | |
| 6535 | + "collabstr_tiktok" | |
| 6536 | + ], | |
| 6537 | + "total_followers": 5600, | |
| 6538 | + "platform_count": 2, | |
| 6539 | + "tier": "nano/inconnu" | |
| 6540 | + }, | |
| 6541 | + { | |
| 6542 | + "name": "Shivani Bansal", | |
| 6543 | + "handles": { | |
| 6544 | + "instagram": "__shivanibansal__", | |
| 6545 | + "tiktok": null, | |
| 6546 | + "youtube": null, | |
| 6547 | + "facebook": null, | |
| 6548 | + "x": null, | |
| 6549 | + "twitch": null | |
| 6550 | + }, | |
| 6551 | + "metrics": { | |
| 6552 | + "instagram_followers": 5500, | |
| 6553 | + "tiktok_followers": null, | |
| 6554 | + "youtube_subscribers": null, | |
| 6555 | + "facebook_followers": null, | |
| 6556 | + "x_followers": null, | |
| 6557 | + "twitch_followers": null | |
| 6558 | + }, | |
| 6559 | + "category": "food", | |
| 6560 | + "location": "Montréal", | |
| 6561 | + "notes": "food, beauté, lifestyle et expériences", | |
| 6562 | + "sources": [ | |
| 6563 | + "feedspot" | |
| 6564 | + ], | |
| 6565 | + "total_followers": 5500, | |
| 6566 | + "platform_count": 1, | |
| 6567 | + "tier": "nano/inconnu" | |
| 6568 | + }, | |
| 6569 | + { | |
| 6570 | + "name": "Jennifer Doré Dallas", | |
| 6571 | + "handles": { | |
| 6572 | + "instagram": "moimessouliers", | |
| 6573 | + "tiktok": null, | |
| 6574 | + "youtube": null, | |
| 6575 | + "facebook": null, | |
| 6576 | + "x": null, | |
| 6577 | + "twitch": null | |
| 6578 | + }, | |
| 6579 | + "metrics": { | |
| 6580 | + "instagram_followers": 5438, | |
| 6581 | + "tiktok_followers": null, | |
| 6582 | + "youtube_subscribers": null, | |
| 6583 | + "facebook_followers": null, | |
| 6584 | + "x_followers": null, | |
| 6585 | + "twitch_followers": null | |
| 6586 | + }, | |
| 6587 | + "category": "voyage", | |
| 6588 | + "location": "Québec", | |
| 6589 | + "notes": "Voyageuse gourmande, blogue Moi, mes souliers", | |
| 6590 | + "sources": [ | |
| 6591 | + "noovomoi_voyage" | |
| 6592 | + ], | |
| 6593 | + "total_followers": 5438, | |
| 6594 | + "platform_count": 1, | |
| 6595 | + "tier": "nano/inconnu" | |
| 6596 | + }, | |
| 6597 | + { | |
| 6598 | + "name": "Rémi Lemieux", | |
| 6599 | + "handles": { | |
| 6600 | + "instagram": "chef.remi.lemieux", | |
| 6601 | + "tiktok": null, | |
| 6602 | + "youtube": null, | |
| 6603 | + "facebook": null, | |
| 6604 | + "x": null, | |
| 6605 | + "twitch": null | |
| 6606 | + }, | |
| 6607 | + "metrics": { | |
| 6608 | + "instagram_followers": 5200, | |
| 6609 | + "tiktok_followers": null, | |
| 6610 | + "youtube_subscribers": null, | |
| 6611 | + "facebook_followers": null, | |
| 6612 | + "x_followers": null, | |
| 6613 | + "twitch_followers": null | |
| 6614 | + }, | |
| 6615 | + "category": "food", | |
| 6616 | + "location": "Montréal", | |
| 6617 | + "notes": "Chef, Guide Michelin 2025; projet resto centre-ville été 2026", | |
| 6618 | + "sources": [ | |
| 6619 | + "feedspot_food" | |
| 6620 | + ], | |
| 6621 | + "total_followers": 5200, | |
| 6622 | + "platform_count": 1, | |
| 6623 | + "tier": "nano/inconnu" | |
| 6624 | + }, | |
| 6625 | + { | |
| 6626 | + "name": "Angela Rasile", | |
| 6627 | + "handles": { | |
| 6628 | + "instagram": "angelarasile", | |
| 6629 | + "tiktok": null, | |
| 6630 | + "youtube": null, | |
| 6631 | + "facebook": null, | |
| 6632 | + "x": null, | |
| 6633 | + "twitch": null | |
| 6634 | + }, | |
| 6635 | + "metrics": { | |
| 6636 | + "instagram_followers": 5000, | |
| 6637 | + "tiktok_followers": null, | |
| 6638 | + "youtube_subscribers": null, | |
| 6639 | + "facebook_followers": null, | |
| 6640 | + "x_followers": null, | |
| 6641 | + "twitch_followers": null | |
| 6642 | + }, | |
| 6643 | + "category": "mode", | |
| 6644 | + "location": "Montréal", | |
| 6645 | + "notes": "mode, beauté et lifestyle", | |
| 6646 | + "sources": [ | |
| 6647 | + "collabstr" | |
| 6648 | + ], | |
| 6649 | + "total_followers": 5000, | |
| 6650 | + "platform_count": 1, | |
| 6651 | + "tier": "nano/inconnu" | |
| 6652 | + }, | |
| 6653 | + { | |
| 6654 | + "name": "Marine Trudel", | |
| 6655 | + "handles": { | |
| 6656 | + "instagram": "marineemmaa", | |
| 6657 | + "tiktok": null, | |
| 6658 | + "youtube": null, | |
| 6659 | + "facebook": null, | |
| 6660 | + "x": null, | |
| 6661 | + "twitch": null | |
| 6662 | + }, | |
| 6663 | + "metrics": { | |
| 6664 | + "instagram_followers": 4900, | |
| 6665 | + "tiktok_followers": null, | |
| 6666 | + "youtube_subscribers": null, | |
| 6667 | + "facebook_followers": null, | |
| 6668 | + "x_followers": null, | |
| 6669 | + "twitch_followers": null | |
| 6670 | + }, | |
| 6671 | + "category": "lifestyle", | |
| 6672 | + "location": "Montréal", | |
| 6673 | + "notes": "lifestyle et bien-être", | |
| 6674 | + "sources": [ | |
| 6675 | + "feedspot" | |
| 6676 | + ], | |
| 6677 | + "total_followers": 4900, | |
| 6678 | + "platform_count": 1, | |
| 6679 | + "tier": "nano/inconnu" | |
| 6680 | + }, | |
| 6681 | + { | |
| 6682 | + "name": "Carla Isin", | |
| 6683 | + "handles": { | |
| 6684 | + "instagram": "carlaisin", | |
| 6685 | + "tiktok": null, | |
| 6686 | + "youtube": null, | |
| 6687 | + "facebook": null, | |
| 6688 | + "x": null, | |
| 6689 | + "twitch": null | |
| 6690 | + }, | |
| 6691 | + "metrics": { | |
| 6692 | + "instagram_followers": 4800, | |
| 6693 | + "tiktok_followers": null, | |
| 6694 | + "youtube_subscribers": null, | |
| 6695 | + "facebook_followers": null, | |
| 6696 | + "x_followers": null, | |
| 6697 | + "twitch_followers": null | |
| 6698 | + }, | |
| 6699 | + "category": "beauté", | |
| 6700 | + "location": "Montréal", | |
| 6701 | + "notes": "beauté, lifestyle, foodie; cofondatrice @xpose.marketing", | |
| 6702 | + "sources": [ | |
| 6703 | + "feedspot" | |
| 6704 | + ], | |
| 6705 | + "total_followers": 4800, | |
| 6706 | + "platform_count": 1, | |
| 6707 | + "tier": "nano/inconnu" | |
| 6708 | + }, | |
| 6709 | + { | |
| 6710 | + "name": "Mirelle Lupovici", | |
| 6711 | + "handles": { | |
| 6712 | + "instagram": "mtlelle", | |
| 6713 | + "tiktok": null, | |
| 6714 | + "youtube": null, | |
| 6715 | + "facebook": null, | |
| 6716 | + "x": null, | |
| 6717 | + "twitch": null | |
| 6718 | + }, | |
| 6719 | + "metrics": { | |
| 6720 | + "instagram_followers": 4800, | |
| 6721 | + "tiktok_followers": null, | |
| 6722 | + "youtube_subscribers": null, | |
| 6723 | + "facebook_followers": null, | |
| 6724 | + "x_followers": null, | |
| 6725 | + "twitch_followers": null | |
| 6726 | + }, | |
| 6727 | + "category": "lifestyle", | |
| 6728 | + "location": "Montréal", | |
| 6729 | + "notes": "#scoliosiswarrior", | |
| 6730 | + "sources": [ | |
| 6731 | + "feedspot" | |
| 6732 | + ], | |
| 6733 | + "total_followers": 4800, | |
| 6734 | + "platform_count": 1, | |
| 6735 | + "tier": "nano/inconnu" | |
| 6736 | + }, | |
| 6737 | + { | |
| 6738 | + "name": "Anthony", | |
| 6739 | + "handles": { | |
| 6740 | + "instagram": "anthony.mtl.creator", | |
| 6741 | + "tiktok": null, | |
| 6742 | + "youtube": null, | |
| 6743 | + "facebook": null, | |
| 6744 | + "x": null, | |
| 6745 | + "twitch": null | |
| 6746 | + }, | |
| 6747 | + "metrics": { | |
| 6748 | + "instagram_followers": 4700, | |
| 6749 | + "tiktok_followers": null, | |
| 6750 | + "youtube_subscribers": null, | |
| 6751 | + "facebook_followers": null, | |
| 6752 | + "x_followers": null, | |
| 6753 | + "twitch_followers": null | |
| 6754 | + }, | |
| 6755 | + "category": "lifestyle", | |
| 6756 | + "location": "Montréal", | |
| 6757 | + "notes": "food, beauté et lifestyle; membre @acrea.officiel | Food, beauté, lifestyle; membre ACREA; +14% de croissance IG en 3 mois", | |
| 6758 | + "sources": [ | |
| 6759 | + "feedspot", | |
| 6760 | + "feedspot_food" | |
| 6761 | + ], | |
| 6762 | + "total_followers": 4700, | |
| 6763 | + "platform_count": 1, | |
| 6764 | + "tier": "nano/inconnu" | |
| 6765 | + }, | |
| 6766 | + { | |
| 6767 | + "name": "Kevarine (ultimate.foodiemtl)", | |
| 6768 | + "handles": { | |
| 6769 | + "instagram": "ultimate.foodiemtl", | |
| 6770 | + "tiktok": null, | |
| 6771 | + "youtube": null, | |
| 6772 | + "facebook": null, | |
| 6773 | + "x": null, | |
| 6774 | + "twitch": null | |
| 6775 | + }, | |
| 6776 | + "metrics": { | |
| 6777 | + "instagram_followers": 4400, | |
| 6778 | + "tiktok_followers": null, | |
| 6779 | + "youtube_subscribers": null, | |
| 6780 | + "facebook_followers": null, | |
| 6781 | + "x_followers": null, | |
| 6782 | + "twitch_followers": null | |
| 6783 | + }, | |
| 6784 | + "category": "food", | |
| 6785 | + "location": "Montréal", | |
| 6786 | + "notes": "Guide food MTL; 11M de vues sur Google (niveau 7)", | |
| 6787 | + "sources": [ | |
| 6788 | + "feedspot_food" | |
| 6789 | + ], | |
| 6790 | + "total_followers": 4400, | |
| 6791 | + "platform_count": 1, | |
| 6792 | + "tier": "nano/inconnu" | |
| 6793 | + }, | |
| 6794 | + { | |
| 6795 | + "name": "Jessika Hall", | |
| 6796 | + "handles": { | |
| 6797 | + "instagram": "bunny_ta", | |
| 6798 | + "tiktok": null, | |
| 6799 | + "youtube": null, | |
| 6800 | + "facebook": null, | |
| 6801 | + "x": null, | |
| 6802 | + "twitch": null | |
| 6803 | + }, | |
| 6804 | + "metrics": { | |
| 6805 | + "instagram_followers": 4200, | |
| 6806 | + "tiktok_followers": null, | |
| 6807 | + "youtube_subscribers": null, | |
| 6808 | + "facebook_followers": null, | |
| 6809 | + "x_followers": null, | |
| 6810 | + "twitch_followers": null | |
| 6811 | + }, | |
| 6812 | + "category": "voyage", | |
| 6813 | + "location": "Montréal", | |
| 6814 | + "notes": "voyage de luxe abordable, hôtels chics", | |
| 6815 | + "sources": [ | |
| 6816 | + "feedspot" | |
| 6817 | + ], | |
| 6818 | + "total_followers": 4200, | |
| 6819 | + "platform_count": 1, | |
| 6820 | + "tier": "nano/inconnu" | |
| 6821 | + }, | |
| 6822 | + { | |
| 6823 | + "name": "Marie-Pier Leduc", | |
| 6824 | + "handles": { | |
| 6825 | + "instagram": "marieisaway", | |
| 6826 | + "tiktok": null, | |
| 6827 | + "youtube": null, | |
| 6828 | + "facebook": null, | |
| 6829 | + "x": null, | |
| 6830 | + "twitch": null | |
| 6831 | + }, | |
| 6832 | + "metrics": { | |
| 6833 | + "instagram_followers": 4000, | |
| 6834 | + "tiktok_followers": null, | |
| 6835 | + "youtube_subscribers": null, | |
| 6836 | + "facebook_followers": null, | |
| 6837 | + "x_followers": null, | |
| 6838 | + "twitch_followers": null | |
| 6839 | + }, | |
| 6840 | + "category": "voyage", | |
| 6841 | + "location": "Québec", | |
| 6842 | + "notes": "Co-fondatrice de Nomade Magazine; moins de 4 000 abonnés IG (2020); #3 du top VoyageurQc", | |
| 6843 | + "sources": [ | |
| 6844 | + "voyageurqc" | |
| 6845 | + ], | |
| 6846 | + "total_followers": 4000, | |
| 6847 | + "platform_count": 1, | |
| 6848 | + "tier": "nano/inconnu" | |
| 6849 | + }, | |
| 6850 | + { | |
| 6851 | + "name": "Jessica Campoli", | |
| 6852 | + "handles": { | |
| 6853 | + "instagram": "jessica_campolii", | |
| 6854 | + "tiktok": null, | |
| 6855 | + "youtube": null, | |
| 6856 | + "facebook": null, | |
| 6857 | + "x": null, | |
| 6858 | + "twitch": null | |
| 6859 | + }, | |
| 6860 | + "metrics": { | |
| 6861 | + "instagram_followers": 3900, | |
| 6862 | + "tiktok_followers": null, | |
| 6863 | + "youtube_subscribers": null, | |
| 6864 | + "facebook_followers": null, | |
| 6865 | + "x_followers": null, | |
| 6866 | + "twitch_followers": null | |
| 6867 | + }, | |
| 6868 | + "category": "mode", | |
| 6869 | + "location": "Montréal", | |
| 6870 | + "notes": "mode et lifestyle; bio mentionne Fort Lauderdale mais listée à Montréal", | |
| 6871 | + "sources": [ | |
| 6872 | + "feedspot" | |
| 6873 | + ], | |
| 6874 | + "total_followers": 3900, | |
| 6875 | + "platform_count": 1, | |
| 6876 | + "tier": "nano/inconnu" | |
| 6877 | + }, | |
| 6878 | + { | |
| 6879 | + "name": "Hanieh Ghasemi", | |
| 6880 | + "handles": { | |
| 6881 | + "instagram": "honeyrose.mademoiselle", | |
| 6882 | + "tiktok": null, | |
| 6883 | + "youtube": null, | |
| 6884 | + "facebook": null, | |
| 6885 | + "x": null, | |
| 6886 | + "twitch": null | |
| 6887 | + }, | |
| 6888 | + "metrics": { | |
| 6889 | + "instagram_followers": 3900, | |
| 6890 | + "tiktok_followers": null, | |
| 6891 | + "youtube_subscribers": null, | |
| 6892 | + "facebook_followers": null, | |
| 6893 | + "x_followers": null, | |
| 6894 | + "twitch_followers": null | |
| 6895 | + }, | |
| 6896 | + "category": "mode", | |
| 6897 | + "location": "Montréal", | |
| 6898 | + "notes": "mode, nature, voyage, études", | |
| 6899 | + "sources": [ | |
| 6900 | + "feedspot" | |
| 6901 | + ], | |
| 6902 | + "total_followers": 3900, | |
| 6903 | + "platform_count": 1, | |
| 6904 | + "tier": "nano/inconnu" | |
| 6905 | + }, | |
| 6906 | + { | |
| 6907 | + "name": "Chef Kai Gannon", | |
| 6908 | + "handles": { | |
| 6909 | + "instagram": "chefkai___", | |
| 6910 | + "tiktok": null, | |
| 6911 | + "youtube": null, | |
| 6912 | + "facebook": null, | |
| 6913 | + "x": null, | |
| 6914 | + "twitch": null | |
| 6915 | + }, | |
| 6916 | + "metrics": { | |
| 6917 | + "instagram_followers": 3800, | |
| 6918 | + "tiktok_followers": null, | |
| 6919 | + "youtube_subscribers": null, | |
| 6920 | + "facebook_followers": null, | |
| 6921 | + "x_followers": null, | |
| 6922 | + "twitch_followers": null | |
| 6923 | + }, | |
| 6924 | + "category": "food", | |
| 6925 | + "location": "Québec", | |
| 6926 | + "notes": "Chef privé, cuisine fusion, traiteur, mariages et événements", | |
| 6927 | + "sources": [ | |
| 6928 | + "feedspot_food" | |
| 6929 | + ], | |
| 6930 | + "total_followers": 3800, | |
| 6931 | + "platform_count": 1, | |
| 6932 | + "tier": "nano/inconnu" | |
| 6933 | + }, | |
| 6934 | + { | |
| 6935 | + "name": "Meghan", | |
| 6936 | + "handles": { | |
| 6937 | + "instagram": "meghanproofficial", | |
| 6938 | + "tiktok": null, | |
| 6939 | + "youtube": null, | |
| 6940 | + "facebook": null, | |
| 6941 | + "x": null, | |
| 6942 | + "twitch": null | |
| 6943 | + }, | |
| 6944 | + "metrics": { | |
| 6945 | + "instagram_followers": 3600, | |
| 6946 | + "tiktok_followers": null, | |
| 6947 | + "youtube_subscribers": null, | |
| 6948 | + "facebook_followers": null, | |
| 6949 | + "x_followers": null, | |
| 6950 | + "twitch_followers": null | |
| 6951 | + }, | |
| 6952 | + "category": "mode", | |
| 6953 | + "location": "Québec", | |
| 6954 | + "notes": "créatrice mode et lifestyle", | |
| 6955 | + "sources": [ | |
| 6956 | + "feedspot" | |
| 6957 | + ], | |
| 6958 | + "total_followers": 3600, | |
| 6959 | + "platform_count": 1, | |
| 6960 | + "tier": "nano/inconnu" | |
| 6961 | + }, | |
| 6962 | + { | |
| 6963 | + "name": "Emily (emilyhungrylab)", | |
| 6964 | + "handles": { | |
| 6965 | + "instagram": "emilyhungrylab", | |
| 6966 | + "tiktok": null, | |
| 6967 | + "youtube": null, | |
| 6968 | + "facebook": null, | |
| 6969 | + "x": null, | |
| 6970 | + "twitch": null | |
| 6971 | + }, | |
| 6972 | + "metrics": { | |
| 6973 | + "instagram_followers": 3400, | |
| 6974 | + "tiktok_followers": null, | |
| 6975 | + "youtube_subscribers": null, | |
| 6976 | + "facebook_followers": null, | |
| 6977 | + "x_followers": null, | |
| 6978 | + "twitch_followers": null | |
| 6979 | + }, | |
| 6980 | + "category": "food", | |
| 6981 | + "location": "Québec", | |
| 6982 | + "notes": "Explore les saveurs à Singapour, Montréal et ailleurs | Food worth your time; aventures culinaires en voyage", | |
| 6983 | + "sources": [ | |
| 6984 | + "feedspot_food" | |
| 6985 | + ], | |
| 6986 | + "total_followers": 3400, | |
| 6987 | + "platform_count": 1, | |
| 6988 | + "tier": "nano/inconnu" | |
| 6989 | + }, | |
| 6990 | + { | |
| 6991 | + "name": "Sarah-Émilie Nault", | |
| 6992 | + "handles": { | |
| 6993 | + "instagram": "sarahemilienault", | |
| 6994 | + "tiktok": null, | |
| 6995 | + "youtube": null, | |
| 6996 | + "facebook": null, | |
| 6997 | + "x": null, | |
| 6998 | + "twitch": null | |
| 6999 | + }, | |
| 7000 | + "metrics": { | |
| 7001 | + "instagram_followers": 3000, | |
| 7002 | + "tiktok_followers": null, | |
| 7003 | + "youtube_subscribers": null, | |
| 7004 | + "facebook_followers": null, | |
| 7005 | + "x_followers": null, | |
| 7006 | + "twitch_followers": null | |
| 7007 | + }, | |
| 7008 | + "category": "voyage", | |
| 7009 | + "location": "Québec", | |
| 7010 | + "notes": "Journaliste voyage constamment en déplacement", | |
| 7011 | + "sources": [ | |
| 7012 | + "noovomoi_voyage" | |
| 7013 | + ], | |
| 7014 | + "total_followers": 3000, | |
| 7015 | + "platform_count": 1, | |
| 7016 | + "tier": "nano/inconnu" | |
| 7017 | + }, | |
| 7018 | + { | |
| 7019 | + "name": "Julie Labrecque", | |
| 7020 | + "handles": { | |
| 7021 | + "instagram": "the.sweetest.chef", | |
| 7022 | + "tiktok": null, | |
| 7023 | + "youtube": null, | |
| 7024 | + "facebook": null, | |
| 7025 | + "x": null, | |
| 7026 | + "twitch": null | |
| 7027 | + }, | |
| 7028 | + "metrics": { | |
| 7029 | + "instagram_followers": 2900, | |
| 7030 | + "tiktok_followers": null, | |
| 7031 | + "youtube_subscribers": null, | |
| 7032 | + "facebook_followers": null, | |
| 7033 | + "x_followers": null, | |
| 7034 | + "twitch_followers": null | |
| 7035 | + }, | |
| 7036 | + "category": "food", | |
| 7037 | + "location": "Waterloo, Québec", | |
| 7038 | + "notes": "Compte pâtisserie (fermé au moment du recensement)", | |
| 7039 | + "sources": [ | |
| 7040 | + "feedspot_food" | |
| 7041 | + ], | |
| 7042 | + "total_followers": 2900, | |
| 7043 | + "platform_count": 1, | |
| 7044 | + "tier": "nano/inconnu" | |
| 7045 | + }, | |
| 7046 | + { | |
| 7047 | + "name": "Ariane Ménard", | |
| 7048 | + "handles": { | |
| 7049 | + "instagram": "ariane.mnrd", | |
| 7050 | + "tiktok": null, | |
| 7051 | + "youtube": null, | |
| 7052 | + "facebook": null, | |
| 7053 | + "x": null, | |
| 7054 | + "twitch": null | |
| 7055 | + }, | |
| 7056 | + "metrics": { | |
| 7057 | + "instagram_followers": 2800, | |
| 7058 | + "tiktok_followers": null, | |
| 7059 | + "youtube_subscribers": null, | |
| 7060 | + "facebook_followers": null, | |
| 7061 | + "x_followers": null, | |
| 7062 | + "twitch_followers": null | |
| 7063 | + }, | |
| 7064 | + "category": "mode", | |
| 7065 | + "location": "Montréal", | |
| 7066 | + "notes": "mode, lifestyle, développement personnel", | |
| 7067 | + "sources": [ | |
| 7068 | + "feedspot" | |
| 7069 | + ], | |
| 7070 | + "total_followers": 2800, | |
| 7071 | + "platform_count": 1, | |
| 7072 | + "tier": "nano/inconnu" | |
| 7073 | + }, | |
| 7074 | + { | |
| 7075 | + "name": "Shalom A Campbell", | |
| 7076 | + "handles": { | |
| 7077 | + "instagram": "shalomcampbelll", | |
| 7078 | + "tiktok": null, | |
| 7079 | + "youtube": null, | |
| 7080 | + "facebook": null, | |
| 7081 | + "x": null, | |
| 7082 | + "twitch": null | |
| 7083 | + }, | |
| 7084 | + "metrics": { | |
| 7085 | + "instagram_followers": 2700, | |
| 7086 | + "tiktok_followers": null, | |
| 7087 | + "youtube_subscribers": null, | |
| 7088 | + "facebook_followers": null, | |
| 7089 | + "x_followers": null, | |
| 7090 | + "twitch_followers": null | |
| 7091 | + }, | |
| 7092 | + "category": "lifestyle", | |
| 7093 | + "location": "Montréal", | |
| 7094 | + "notes": "vlogs, foodie et self-care; acteur de formation classique", | |
| 7095 | + "sources": [ | |
| 7096 | + "collabstr" | |
| 7097 | + ], | |
| 7098 | + "total_followers": 2700, | |
| 7099 | + "platform_count": 1, | |
| 7100 | + "tier": "nano/inconnu" | |
| 7101 | + }, | |
| 7102 | + { | |
| 7103 | + "name": "Ashley (mtlmarz)", | |
| 7104 | + "handles": { | |
| 7105 | + "instagram": "mtlmarz", | |
| 7106 | + "tiktok": null, | |
| 7107 | + "youtube": null, | |
| 7108 | + "facebook": null, | |
| 7109 | + "x": null, | |
| 7110 | + "twitch": null | |
| 7111 | + }, | |
| 7112 | + "metrics": { | |
| 7113 | + "instagram_followers": 2700, | |
| 7114 | + "tiktok_followers": null, | |
| 7115 | + "youtube_subscribers": null, | |
| 7116 | + "facebook_followers": null, | |
| 7117 | + "x_followers": null, | |
| 7118 | + "twitch_followers": null | |
| 7119 | + }, | |
| 7120 | + "category": "food", | |
| 7121 | + "location": "Montréal", | |
| 7122 | + "notes": "Partage ses spots préférés pour manger et explorer", | |
| 7123 | + "sources": [ | |
| 7124 | + "feedspot_food" | |
| 7125 | + ], | |
| 7126 | + "total_followers": 2700, | |
| 7127 | + "platform_count": 1, | |
| 7128 | + "tier": "nano/inconnu" | |
| 7129 | + }, | |
| 7130 | + { | |
| 7131 | + "name": "Sneha", | |
| 7132 | + "handles": { | |
| 7133 | + "instagram": "sneha.travels.journey", | |
| 7134 | + "tiktok": null, | |
| 7135 | + "youtube": null, | |
| 7136 | + "facebook": null, | |
| 7137 | + "x": null, | |
| 7138 | + "twitch": null | |
| 7139 | + }, | |
| 7140 | + "metrics": { | |
| 7141 | + "instagram_followers": 2600, | |
| 7142 | + "tiktok_followers": null, | |
| 7143 | + "youtube_subscribers": null, | |
| 7144 | + "facebook_followers": null, | |
| 7145 | + "x_followers": null, | |
| 7146 | + "twitch_followers": null | |
| 7147 | + }, | |
| 7148 | + "category": "voyage", | |
| 7149 | + "location": "Montréal", | |
| 7150 | + "notes": "guides de voyage et bonnes adresses à Montréal, partenaire MTL Blog/Narcity", | |
| 7151 | + "sources": [ | |
| 7152 | + "feedspot" | |
| 7153 | + ], | |
| 7154 | + "total_followers": 2600, | |
| 7155 | + "platform_count": 1, | |
| 7156 | + "tier": "nano/inconnu" | |
| 7157 | + }, | |
| 7158 | + { | |
| 7159 | + "name": "Sheila Luong", | |
| 7160 | + "handles": { | |
| 7161 | + "instagram": "sheilaluongxo", | |
| 7162 | + "tiktok": null, | |
| 7163 | + "youtube": null, | |
| 7164 | + "facebook": null, | |
| 7165 | + "x": null, | |
| 7166 | + "twitch": null | |
| 7167 | + }, | |
| 7168 | + "metrics": { | |
| 7169 | + "instagram_followers": 2600, | |
| 7170 | + "tiktok_followers": null, | |
| 7171 | + "youtube_subscribers": null, | |
| 7172 | + "facebook_followers": null, | |
| 7173 | + "x_followers": null, | |
| 7174 | + "twitch_followers": null | |
| 7175 | + }, | |
| 7176 | + "category": "autre", | |
| 7177 | + "location": "Montréal", | |
| 7178 | + "notes": "conseils de vie et relations, fitness", | |
| 7179 | + "sources": [ | |
| 7180 | + "collabstr" | |
| 7181 | + ], | |
| 7182 | + "total_followers": 2600, | |
| 7183 | + "platform_count": 1, | |
| 7184 | + "tier": "nano/inconnu" | |
| 7185 | + }, | |
| 7186 | + { | |
| 7187 | + "name": "Carmen Li", | |
| 7188 | + "handles": { | |
| 7189 | + "instagram": "carmennlii", | |
| 7190 | + "tiktok": null, | |
| 7191 | + "youtube": null, | |
| 7192 | + "facebook": null, | |
| 7193 | + "x": null, | |
| 7194 | + "twitch": null | |
| 7195 | + }, | |
| 7196 | + "metrics": { | |
| 7197 | + "instagram_followers": 2500, | |
| 7198 | + "tiktok_followers": null, | |
| 7199 | + "youtube_subscribers": null, | |
| 7200 | + "facebook_followers": null, | |
| 7201 | + "x_followers": null, | |
| 7202 | + "twitch_followers": null | |
| 7203 | + }, | |
| 7204 | + "category": "lifestyle", | |
| 7205 | + "location": "Montréal", | |
| 7206 | + "notes": "nano-influenceuse lifestyle", | |
| 7207 | + "sources": [ | |
| 7208 | + "feedspot" | |
| 7209 | + ], | |
| 7210 | + "total_followers": 2500, | |
| 7211 | + "platform_count": 1, | |
| 7212 | + "tier": "nano/inconnu" | |
| 7213 | + }, | |
| 7214 | + { | |
| 7215 | + "name": "ElijahDes12", | |
| 7216 | + "handles": { | |
| 7217 | + "instagram": "elijah_des", | |
| 7218 | + "tiktok": null, | |
| 7219 | + "youtube": null, | |
| 7220 | + "facebook": null, | |
| 7221 | + "x": null, | |
| 7222 | + "twitch": "elijahdes12" | |
| 7223 | + }, | |
| 7224 | + "metrics": { | |
| 7225 | + "instagram_followers": null, | |
| 7226 | + "tiktok_followers": null, | |
| 7227 | + "youtube_subscribers": null, | |
| 7228 | + "facebook_followers": null, | |
| 7229 | + "x_followers": null, | |
| 7230 | + "twitch_followers": 2400 | |
| 7231 | + }, | |
| 7232 | + "category": "gaming", | |
| 7233 | + "location": "Québec", | |
| 7234 | + "notes": "Streamer émergent, contenu varié, slogan GO CRAZY; ~2 400 abonnés Twitch", | |
| 7235 | + "sources": [ | |
| 7236 | + "leregard_twitch" | |
| 7237 | + ], | |
| 7238 | + "total_followers": 2400, | |
| 7239 | + "platform_count": 2, | |
| 7240 | + "tier": "nano/inconnu" | |
| 7241 | + }, | |
| 7242 | + { | |
| 7243 | + "name": "Kate Hume", | |
| 7244 | + "handles": { | |
| 7245 | + "instagram": "katereads95", | |
| 7246 | + "tiktok": null, | |
| 7247 | + "youtube": null, | |
| 7248 | + "facebook": null, | |
| 7249 | + "x": null, | |
| 7250 | + "twitch": null | |
| 7251 | + }, | |
| 7252 | + "metrics": { | |
| 7253 | + "instagram_followers": 1500, | |
| 7254 | + "tiktok_followers": 533, | |
| 7255 | + "youtube_subscribers": null, | |
| 7256 | + "facebook_followers": null, | |
| 7257 | + "x_followers": null, | |
| 7258 | + "twitch_followers": null | |
| 7259 | + }, | |
| 7260 | + "category": "lifestyle", | |
| 7261 | + "location": "Montréal", | |
| 7262 | + "notes": "livres (bookstagram) et lifestyle, UGC", | |
| 7263 | + "sources": [ | |
| 7264 | + "collabstr" | |
| 7265 | + ], | |
| 7266 | + "total_followers": 2033, | |
| 7267 | + "platform_count": 2, | |
| 7268 | + "tier": "nano/inconnu" | |
| 7269 | + }, | |
| 7270 | + { | |
| 7271 | + "name": "Diana Vanegas", | |
| 7272 | + "handles": { | |
| 7273 | + "instagram": "bulcari", | |
| 7274 | + "tiktok": null, | |
| 7275 | + "youtube": null, | |
| 7276 | + "facebook": null, | |
| 7277 | + "x": null, | |
| 7278 | + "twitch": null | |
| 7279 | + }, | |
| 7280 | + "metrics": { | |
| 7281 | + "instagram_followers": 2000, | |
| 7282 | + "tiktok_followers": null, | |
| 7283 | + "youtube_subscribers": null, | |
| 7284 | + "facebook_followers": null, | |
| 7285 | + "x_followers": null, | |
| 7286 | + "twitch_followers": null | |
| 7287 | + }, | |
| 7288 | + "category": "lifestyle", | |
| 7289 | + "location": "Montréal", | |
| 7290 | + "notes": "nano-influenceuse montréalaise", | |
| 7291 | + "sources": [ | |
| 7292 | + "feedspot" | |
| 7293 | + ], | |
| 7294 | + "total_followers": 2000, | |
| 7295 | + "platform_count": 1, | |
| 7296 | + "tier": "nano/inconnu" | |
| 7297 | + }, | |
| 7298 | + { | |
| 7299 | + "name": "Sophie Vixamar Victor", | |
| 7300 | + "handles": { | |
| 7301 | + "instagram": null, | |
| 7302 | + "tiktok": "vixsophie", | |
| 7303 | + "youtube": null, | |
| 7304 | + "facebook": null, | |
| 7305 | + "x": null, | |
| 7306 | + "twitch": null | |
| 7307 | + }, | |
| 7308 | + "metrics": { | |
| 7309 | + "instagram_followers": null, | |
| 7310 | + "tiktok_followers": 2000, | |
| 7311 | + "youtube_subscribers": null, | |
| 7312 | + "facebook_followers": null, | |
| 7313 | + "x_followers": null, | |
| 7314 | + "twitch_followers": null | |
| 7315 | + }, | |
| 7316 | + "category": "lifestyle", | |
| 7317 | + "location": "Québec", | |
| 7318 | + "notes": "Créatrice UGC bilingue (français/anglais) : lifestyle, beauté, produits du quotidien.", | |
| 7319 | + "sources": [ | |
| 7320 | + "collabstr_tiktok" | |
| 7321 | + ], | |
| 7322 | + "total_followers": 2000, | |
| 7323 | + "platform_count": 1, | |
| 7324 | + "tier": "nano/inconnu" | |
| 7325 | + }, | |
| 7326 | + { | |
| 7327 | + "name": "GFZ Parfait", | |
| 7328 | + "handles": { | |
| 7329 | + "instagram": "gfz.parfait", | |
| 7330 | + "tiktok": "parfait.clip", | |
| 7331 | + "youtube": null, | |
| 7332 | + "facebook": null, | |
| 7333 | + "x": null, | |
| 7334 | + "twitch": "gfz_parfait" | |
| 7335 | + }, | |
| 7336 | + "metrics": { | |
| 7337 | + "instagram_followers": 2000, | |
| 7338 | + "tiktok_followers": null, | |
| 7339 | + "youtube_subscribers": null, | |
| 7340 | + "facebook_followers": null, | |
| 7341 | + "x_followers": null, | |
| 7342 | + "twitch_followers": null | |
| 7343 | + }, | |
| 7344 | + "category": "gaming", | |
| 7345 | + "location": "Québec", | |
| 7346 | + "notes": "Prank calls et vidéos de réaction sur Twitch; chaîne TikTok parfait.clip virale", | |
| 7347 | + "sources": [ | |
| 7348 | + "leregard_twitch" | |
| 7349 | + ], | |
| 7350 | + "total_followers": 2000, | |
| 7351 | + "platform_count": 3, | |
| 7352 | + "tier": "nano/inconnu" | |
| 7353 | + }, | |
| 7354 | + { | |
| 7355 | + "name": "Fabio Sperduti", | |
| 7356 | + "handles": { | |
| 7357 | + "instagram": "chefsperduti", | |
| 7358 | + "tiktok": null, | |
| 7359 | + "youtube": null, | |
| 7360 | + "facebook": null, | |
| 7361 | + "x": null, | |
| 7362 | + "twitch": null | |
| 7363 | + }, | |
| 7364 | + "metrics": { | |
| 7365 | + "instagram_followers": 1900, | |
| 7366 | + "tiktok_followers": null, | |
| 7367 | + "youtube_subscribers": null, | |
| 7368 | + "facebook_followers": null, | |
| 7369 | + "x_followers": null, | |
| 7370 | + "twitch_followers": null | |
| 7371 | + }, | |
| 7372 | + "category": "food", | |
| 7373 | + "location": "Montréal", | |
| 7374 | + "notes": "Directeur culinaire Bossa MTL", | |
| 7375 | + "sources": [ | |
| 7376 | + "feedspot_food" | |
| 7377 | + ], | |
| 7378 | + "total_followers": 1900, | |
| 7379 | + "platform_count": 1, | |
| 7380 | + "tier": "nano/inconnu" | |
| 7381 | + }, | |
| 7382 | + { | |
| 7383 | + "name": "Alex (alexfoodi)", | |
| 7384 | + "handles": { | |
| 7385 | + "instagram": "alexfoodi", | |
| 7386 | + "tiktok": null, | |
| 7387 | + "youtube": null, | |
| 7388 | + "facebook": null, | |
| 7389 | + "x": null, | |
| 7390 | + "twitch": null | |
| 7391 | + }, | |
| 7392 | + "metrics": { | |
| 7393 | + "instagram_followers": 1900, | |
| 7394 | + "tiktok_followers": null, | |
| 7395 | + "youtube_subscribers": null, | |
| 7396 | + "facebook_followers": null, | |
| 7397 | + "x_followers": null, | |
| 7398 | + "twitch_followers": null | |
| 7399 | + }, | |
| 7400 | + "category": "food", | |
| 7401 | + "location": "Montréal", | |
| 7402 | + "notes": "Pharmacien le jour, créateur de contenu le soir; voyage et food", | |
| 7403 | + "sources": [ | |
| 7404 | + "feedspot_food" | |
| 7405 | + ], | |
| 7406 | + "total_followers": 1900, | |
| 7407 | + "platform_count": 1, | |
| 7408 | + "tier": "nano/inconnu" | |
| 7409 | + }, | |
| 7410 | + { | |
| 7411 | + "name": "Maria Morari", | |
| 7412 | + "handles": { | |
| 7413 | + "instagram": "morari.mm", | |
| 7414 | + "tiktok": null, | |
| 7415 | + "youtube": null, | |
| 7416 | + "facebook": null, | |
| 7417 | + "x": null, | |
| 7418 | + "twitch": null | |
| 7419 | + }, | |
| 7420 | + "metrics": { | |
| 7421 | + "instagram_followers": 1200, | |
| 7422 | + "tiktok_followers": 189, | |
| 7423 | + "youtube_subscribers": 193, | |
| 7424 | + "facebook_followers": null, | |
| 7425 | + "x_followers": null, | |
| 7426 | + "twitch_followers": null | |
| 7427 | + }, | |
| 7428 | + "category": "fitness", | |
| 7429 | + "location": "Montréal", | |
| 7430 | + "notes": "ex-ballerine professionnelle, fitness postpartum et Pilates; Top Creator Collabstr", | |
| 7431 | + "sources": [ | |
| 7432 | + "collabstr" | |
| 7433 | + ], | |
| 7434 | + "total_followers": 1582, | |
| 7435 | + "platform_count": 3, | |
| 7436 | + "tier": "nano/inconnu" | |
| 7437 | + }, | |
| 7438 | + { | |
| 7439 | + "name": "Carole-Ann Tavano", | |
| 7440 | + "handles": { | |
| 7441 | + "instagram": "ca_tavano", | |
| 7442 | + "tiktok": null, | |
| 7443 | + "youtube": null, | |
| 7444 | + "facebook": null, | |
| 7445 | + "x": null, | |
| 7446 | + "twitch": null | |
| 7447 | + }, | |
| 7448 | + "metrics": { | |
| 7449 | + "instagram_followers": 1500, | |
| 7450 | + "tiktok_followers": null, | |
| 7451 | + "youtube_subscribers": null, | |
| 7452 | + "facebook_followers": null, | |
| 7453 | + "x_followers": null, | |
| 7454 | + "twitch_followers": null | |
| 7455 | + }, | |
| 7456 | + "category": "voyage", | |
| 7457 | + "location": "Québec", | |
| 7458 | + "notes": "voyage et lifestyle bien-être; Top Creator Collabstr", | |
| 7459 | + "sources": [ | |
| 7460 | + "collabstr" | |
| 7461 | + ], | |
| 7462 | + "total_followers": 1500, | |
| 7463 | + "platform_count": 1, | |
| 7464 | + "tier": "nano/inconnu" | |
| 7465 | + }, | |
| 7466 | + { | |
| 7467 | + "name": "Valérie Gagné", | |
| 7468 | + "handles": { | |
| 7469 | + "instagram": "_therunningpanda", | |
| 7470 | + "tiktok": null, | |
| 7471 | + "youtube": null, | |
| 7472 | + "facebook": null, | |
| 7473 | + "x": null, | |
| 7474 | + "twitch": null | |
| 7475 | + }, | |
| 7476 | + "metrics": { | |
| 7477 | + "instagram_followers": 1400, | |
| 7478 | + "tiktok_followers": null, | |
| 7479 | + "youtube_subscribers": null, | |
| 7480 | + "facebook_followers": null, | |
| 7481 | + "x_followers": null, | |
| 7482 | + "twitch_followers": null | |
| 7483 | + }, | |
| 7484 | + "category": "voyage", | |
| 7485 | + "location": "Québec", | |
| 7486 | + "notes": "guide d'aventure et exploratrice (Arctique canadien, Atacama, Baïkal)", | |
| 7487 | + "sources": [ | |
| 7488 | + "collabstr" | |
| 7489 | + ], | |
| 7490 | + "total_followers": 1400, | |
| 7491 | + "platform_count": 1, | |
| 7492 | + "tier": "nano/inconnu" | |
| 7493 | + }, | |
| 7494 | + { | |
| 7495 | + "name": "Ben Schmautz", | |
| 7496 | + "handles": { | |
| 7497 | + "instagram": "ben_schmautz", | |
| 7498 | + "tiktok": null, | |
| 7499 | + "youtube": null, | |
| 7500 | + "facebook": null, | |
| 7501 | + "x": null, | |
| 7502 | + "twitch": null | |
| 7503 | + }, | |
| 7504 | + "metrics": { | |
| 7505 | + "instagram_followers": 860, | |
| 7506 | + "tiktok_followers": null, | |
| 7507 | + "youtube_subscribers": null, | |
| 7508 | + "facebook_followers": null, | |
| 7509 | + "x_followers": null, | |
| 7510 | + "twitch_followers": null | |
| 7511 | + }, | |
| 7512 | + "category": "voyage", | |
| 7513 | + "location": "Québec", | |
| 7514 | + "notes": "Photographe et journaliste professionnel de voyage", | |
| 7515 | + "sources": [ | |
| 7516 | + "noovomoi_voyage" | |
| 7517 | + ], | |
| 7518 | + "total_followers": 860, | |
| 7519 | + "platform_count": 1, | |
| 7520 | + "tier": "nano/inconnu" | |
| 7521 | + }, | |
| 7522 | + { | |
| 7523 | + "name": "Chris Woo", | |
| 7524 | + "handles": { | |
| 7525 | + "instagram": "therealchefwoo", | |
| 7526 | + "tiktok": null, | |
| 7527 | + "youtube": null, | |
| 7528 | + "facebook": null, | |
| 7529 | + "x": null, | |
| 7530 | + "twitch": null | |
| 7531 | + }, | |
| 7532 | + "metrics": { | |
| 7533 | + "instagram_followers": 557, | |
| 7534 | + "tiktok_followers": null, | |
| 7535 | + "youtube_subscribers": null, | |
| 7536 | + "facebook_followers": null, | |
| 7537 | + "x_followers": null, | |
| 7538 | + "twitch_followers": null | |
| 7539 | + }, | |
| 7540 | + "category": "food", | |
| 7541 | + "location": "Québec", | |
| 7542 | + "notes": "Chef, maman, blogueuse culinaire et jardinière", | |
| 7543 | + "sources": [ | |
| 7544 | + "feedspot_food" | |
| 7545 | + ], | |
| 7546 | + "total_followers": 557, | |
| 7547 | + "platform_count": 1, | |
| 7548 | + "tier": "nano/inconnu" | |
| 7549 | + }, | |
| 7550 | + { | |
| 7551 | + "name": "Cath Bastien", | |
| 7552 | + "handles": { | |
| 7553 | + "instagram": null, | |
| 7554 | + "tiktok": null, | |
| 7555 | + "youtube": null, | |
| 7556 | + "facebook": null, | |
| 7557 | + "x": null, | |
| 7558 | + "twitch": null | |
| 7559 | + }, | |
| 7560 | + "metrics": { | |
| 7561 | + "instagram_followers": null, | |
| 7562 | + "tiktok_followers": null, | |
| 7563 | + "youtube_subscribers": null, | |
| 7564 | + "facebook_followers": null, | |
| 7565 | + "x_followers": null, | |
| 7566 | + "twitch_followers": null | |
| 7567 | + }, | |
| 7568 | + "category": "fitness", | |
| 7569 | + "location": "Québec", | |
| 7570 | + "notes": "influenceuse fitness mentionnée dans l'article Billie (ex-copine de Marc Fitt)", | |
| 7571 | + "sources": [ | |
| 7572 | + "billie" | |
| 7573 | + ], | |
| 7574 | + "total_followers": null, | |
| 7575 | + "platform_count": 0, | |
| 7576 | + "tier": "nano/inconnu" | |
| 7577 | + }, | |
| 7578 | + { | |
| 7579 | + "name": "Noémie Lacerte", | |
| 7580 | + "handles": { | |
| 7581 | + "instagram": null, | |
| 7582 | + "tiktok": null, | |
| 7583 | + "youtube": null, | |
| 7584 | + "facebook": null, | |
| 7585 | + "x": null, | |
| 7586 | + "twitch": null | |
| 7587 | + }, | |
| 7588 | + "metrics": { | |
| 7589 | + "instagram_followers": null, | |
| 7590 | + "tiktok_followers": null, | |
| 7591 | + "youtube_subscribers": null, | |
| 7592 | + "facebook_followers": null, | |
| 7593 | + "x_followers": null, | |
| 7594 | + "twitch_followers": null | |
| 7595 | + }, | |
| 7596 | + "category": "mode", | |
| 7597 | + "location": "Québec", | |
| 7598 | + "notes": "influenceuse mode mentionnée sur Billie (marque Noble) | Créatrice de contenu adulte (OnlyFans), forte présence sociale; revenus estimés 100k$–400k$ (canadult).", | |
| 7599 | + "sources": [ | |
| 7600 | + "billie", | |
| 7601 | + "canadult" | |
| 7602 | + ], | |
| 7603 | + "total_followers": null, | |
| 7604 | + "platform_count": 0, | |
| 7605 | + "tier": "nano/inconnu" | |
| 7606 | + }, | |
| 7607 | + { | |
| 7608 | + "name": "Zoé Duval", | |
| 7609 | + "handles": { | |
| 7610 | + "instagram": null, | |
| 7611 | + "tiktok": null, | |
| 7612 | + "youtube": null, | |
| 7613 | + "facebook": null, | |
| 7614 | + "x": null, | |
| 7615 | + "twitch": null | |
| 7616 | + }, | |
| 7617 | + "metrics": { | |
| 7618 | + "instagram_followers": null, | |
| 7619 | + "tiktok_followers": null, | |
| 7620 | + "youtube_subscribers": null, | |
| 7621 | + "facebook_followers": null, | |
| 7622 | + "x_followers": null, | |
| 7623 | + "twitch_followers": null | |
| 7624 | + }, | |
| 7625 | + "category": "autre", | |
| 7626 | + "location": "Québec", | |
| 7627 | + "notes": "personnalité web apparaissant dans les vidéos Billie (greenflag/redflag) | Créatrice mentionnée sur Billie (vidéo « Est-ce un greenflag ou un redflag ? »); mention périphérique, peu de données.", | |
| 7628 | + "sources": [ | |
| 7629 | + "billie", | |
| 7630 | + "billie_gala" | |
| 7631 | + ], | |
| 7632 | + "total_followers": null, | |
| 7633 | + "platform_count": 0, | |
| 7634 | + "tier": "nano/inconnu" | |
| 7635 | + }, | |
| 7636 | + { | |
| 7637 | + "name": "Teagan Cain", | |
| 7638 | + "handles": { | |
| 7639 | + "instagram": "teagan.cain", | |
| 7640 | + "tiktok": null, | |
| 7641 | + "youtube": null, | |
| 7642 | + "facebook": null, | |
| 7643 | + "x": null, | |
| 7644 | + "twitch": null | |
| 7645 | + }, | |
| 7646 | + "metrics": { | |
| 7647 | + "instagram_followers": null, | |
| 7648 | + "tiktok_followers": null, | |
| 7649 | + "youtube_subscribers": null, | |
| 7650 | + "facebook_followers": null, | |
| 7651 | + "x_followers": null, | |
| 7652 | + "twitch_followers": null | |
| 7653 | + }, | |
| 7654 | + "category": "mode", | |
| 7655 | + "location": "Montréal", | |
| 7656 | + "notes": "mannequin, étudiante en économie et foodie; skincare et marques locales", | |
| 7657 | + "sources": [ | |
| 7658 | + "embold" | |
| 7659 | + ], | |
| 7660 | + "total_followers": null, | |
| 7661 | + "platform_count": 1, | |
| 7662 | + "tier": "nano/inconnu" | |
| 7663 | + }, | |
| 7664 | + { | |
| 7665 | + "name": "Priscila Sanches", | |
| 7666 | + "handles": { | |
| 7667 | + "instagram": "pr_scila", | |
| 7668 | + "tiktok": null, | |
| 7669 | + "youtube": null, | |
| 7670 | + "facebook": null, | |
| 7671 | + "x": null, | |
| 7672 | + "twitch": null | |
| 7673 | + }, | |
| 7674 | + "metrics": { | |
| 7675 | + "instagram_followers": null, | |
| 7676 | + "tiktok_followers": null, | |
| 7677 | + "youtube_subscribers": null, | |
| 7678 | + "facebook_followers": null, | |
| 7679 | + "x_followers": null, | |
| 7680 | + "twitch_followers": null | |
| 7681 | + }, | |
| 7682 | + "category": "voyage", | |
| 7683 | + "location": "Montréal", | |
| 7684 | + "notes": "Brésilienne de 28 ans; voyage, beauté, lifestyle et mode", | |
| 7685 | + "sources": [ | |
| 7686 | + "embold" | |
| 7687 | + ], | |
| 7688 | + "total_followers": null, | |
| 7689 | + "platform_count": 1, | |
| 7690 | + "tier": "nano/inconnu" | |
| 7691 | + }, | |
| 7692 | + { | |
| 7693 | + "name": "Katie DiCaprio", | |
| 7694 | + "handles": { | |
| 7695 | + "instagram": "mtlkatie", | |
| 7696 | + "tiktok": null, | |
| 7697 | + "youtube": null, | |
| 7698 | + "facebook": null, | |
| 7699 | + "x": null, | |
| 7700 | + "twitch": null | |
| 7701 | + }, | |
| 7702 | + "metrics": { | |
| 7703 | + "instagram_followers": null, | |
| 7704 | + "tiktok_followers": null, | |
| 7705 | + "youtube_subscribers": null, | |
| 7706 | + "facebook_followers": null, | |
| 7707 | + "x_followers": null, | |
| 7708 | + "twitch_followers": null | |
| 7709 | + }, | |
| 7710 | + "category": "autre", | |
| 7711 | + "location": "Montréal", | |
| 7712 | + "notes": "marketing sportif, conférences, mode et podcast", | |
| 7713 | + "sources": [ | |
| 7714 | + "embold" | |
| 7715 | + ], | |
| 7716 | + "total_followers": null, | |
| 7717 | + "platform_count": 1, | |
| 7718 | + "tier": "nano/inconnu" | |
| 7719 | + }, | |
| 7720 | + { | |
| 7721 | + "name": "Sarah Nahle", | |
| 7722 | + "handles": { | |
| 7723 | + "instagram": "sarahnahlee", | |
| 7724 | + "tiktok": null, | |
| 7725 | + "youtube": null, | |
| 7726 | + "facebook": null, | |
| 7727 | + "x": null, | |
| 7728 | + "twitch": null | |
| 7729 | + }, | |
| 7730 | + "metrics": { | |
| 7731 | + "instagram_followers": null, | |
| 7732 | + "tiktok_followers": null, | |
| 7733 | + "youtube_subscribers": null, | |
| 7734 | + "facebook_followers": null, | |
| 7735 | + "x_followers": null, | |
| 7736 | + "twitch_followers": null | |
| 7737 | + }, | |
| 7738 | + "category": "lifestyle", | |
| 7739 | + "location": "Montréal", | |
| 7740 | + "notes": "maman de 2 filles de moins de 2 ans", | |
| 7741 | + "sources": [ | |
| 7742 | + "embold" | |
| 7743 | + ], | |
| 7744 | + "total_followers": null, | |
| 7745 | + "platform_count": 1, | |
| 7746 | + "tier": "nano/inconnu" | |
| 7747 | + }, | |
| 7748 | + { | |
| 7749 | + "name": "Melisa", | |
| 7750 | + "handles": { | |
| 7751 | + "instagram": "meligatorr", | |
| 7752 | + "tiktok": null, | |
| 7753 | + "youtube": null, | |
| 7754 | + "facebook": null, | |
| 7755 | + "x": null, | |
| 7756 | + "twitch": null | |
| 7757 | + }, | |
| 7758 | + "metrics": { | |
| 7759 | + "instagram_followers": null, | |
| 7760 | + "tiktok_followers": null, | |
| 7761 | + "youtube_subscribers": null, | |
| 7762 | + "facebook_followers": null, | |
| 7763 | + "x_followers": null, | |
| 7764 | + "twitch_followers": null | |
| 7765 | + }, | |
| 7766 | + "category": "lifestyle", | |
| 7767 | + "location": "Montréal", | |
| 7768 | + "notes": "blogueuse lifestyle montréalaise", | |
| 7769 | + "sources": [ | |
| 7770 | + "embold" | |
| 7771 | + ], | |
| 7772 | + "total_followers": null, | |
| 7773 | + "platform_count": 1, | |
| 7774 | + "tier": "nano/inconnu" | |
| 7775 | + }, | |
| 7776 | + { | |
| 7777 | + "name": "Gabrielle Marion", | |
| 7778 | + "handles": { | |
| 7779 | + "instagram": null, | |
| 7780 | + "tiktok": null, | |
| 7781 | + "youtube": "gabrielle marion", | |
| 7782 | + "facebook": null, | |
| 7783 | + "x": null, | |
| 7784 | + "twitch": null | |
| 7785 | + }, | |
| 7786 | + "metrics": { | |
| 7787 | + "instagram_followers": null, | |
| 7788 | + "tiktok_followers": null, | |
| 7789 | + "youtube_subscribers": null, | |
| 7790 | + "facebook_followers": null, | |
| 7791 | + "x_followers": null, | |
| 7792 | + "twitch_followers": null | |
| 7793 | + }, | |
| 7794 | + "category": "lifestyle", | |
| 7795 | + "location": "Québec", | |
| 7796 | + "notes": "YouTubeuse québécoise mentionnée dans des articles connexes de Billie (aucune métrique dans la source).", | |
| 7797 | + "sources": [ | |
| 7798 | + "billie_youtube" | |
| 7799 | + ], | |
| 7800 | + "total_followers": null, | |
| 7801 | + "platform_count": 1, | |
| 7802 | + "tier": "nano/inconnu" | |
| 7803 | + }, | |
| 7804 | + { | |
| 7805 | + "name": "Mila Taillefer", | |
| 7806 | + "handles": { | |
| 7807 | + "instagram": null, | |
| 7808 | + "tiktok": null, | |
| 7809 | + "youtube": "mila taillefer", | |
| 7810 | + "facebook": null, | |
| 7811 | + "x": null, | |
| 7812 | + "twitch": null | |
| 7813 | + }, | |
| 7814 | + "metrics": { | |
| 7815 | + "instagram_followers": null, | |
| 7816 | + "tiktok_followers": null, | |
| 7817 | + "youtube_subscribers": null, | |
| 7818 | + "facebook_followers": null, | |
| 7819 | + "x_followers": null, | |
| 7820 | + "twitch_followers": null | |
| 7821 | + }, | |
| 7822 | + "category": "lifestyle", | |
| 7823 | + "location": "Québec", | |
| 7824 | + "notes": "Ex-YouTubeuse; mentionnée dans un article connexe de Billie sur son nouveau projet après YouTube (aucune métrique dans la source).", | |
| 7825 | + "sources": [ | |
| 7826 | + "billie_youtube" | |
| 7827 | + ], | |
| 7828 | + "total_followers": null, | |
| 7829 | + "platform_count": 1, | |
| 7830 | + "tier": "nano/inconnu" | |
| 7831 | + }, | |
| 7832 | + { | |
| 7833 | + "name": "Cassandra Bouchard", | |
| 7834 | + "handles": { | |
| 7835 | + "instagram": null, | |
| 7836 | + "tiktok": null, | |
| 7837 | + "youtube": null, | |
| 7838 | + "facebook": null, | |
| 7839 | + "x": null, | |
| 7840 | + "twitch": null | |
| 7841 | + }, | |
| 7842 | + "metrics": { | |
| 7843 | + "instagram_followers": null, | |
| 7844 | + "tiktok_followers": null, | |
| 7845 | + "youtube_subscribers": null, | |
| 7846 | + "facebook_followers": null, | |
| 7847 | + "x_followers": null, | |
| 7848 | + "twitch_followers": null | |
| 7849 | + }, | |
| 7850 | + "category": "lifestyle", | |
| 7851 | + "location": "Québec", | |
| 7852 | + "notes": "Influenceuse québécoise mentionnée dans un article connexe de Billie (aucune métrique dans la source).", | |
| 7853 | + "sources": [ | |
| 7854 | + "billie_youtube" | |
| 7855 | + ], | |
| 7856 | + "total_followers": null, | |
| 7857 | + "platform_count": 0, | |
| 7858 | + "tier": "nano/inconnu" | |
| 7859 | + }, | |
| 7860 | + { | |
| 7861 | + "name": "Catherine Bastien", | |
| 7862 | + "handles": { | |
| 7863 | + "instagram": null, | |
| 7864 | + "tiktok": null, | |
| 7865 | + "youtube": null, | |
| 7866 | + "facebook": null, | |
| 7867 | + "x": null, | |
| 7868 | + "twitch": null | |
| 7869 | + }, | |
| 7870 | + "metrics": { | |
| 7871 | + "instagram_followers": null, | |
| 7872 | + "tiktok_followers": null, | |
| 7873 | + "youtube_subscribers": null, | |
| 7874 | + "facebook_followers": null, | |
| 7875 | + "x_followers": null, | |
| 7876 | + "twitch_followers": null | |
| 7877 | + }, | |
| 7878 | + "category": "lifestyle", | |
| 7879 | + "location": "Québec", | |
| 7880 | + "notes": "Influenceuse québécoise mentionnée dans un article connexe de Billie (aucune métrique dans la source).", | |
| 7881 | + "sources": [ | |
| 7882 | + "billie_youtube" | |
| 7883 | + ], | |
| 7884 | + "total_followers": null, | |
| 7885 | + "platform_count": 0, | |
| 7886 | + "tier": "nano/inconnu" | |
| 7887 | + }, | |
| 7888 | + { | |
| 7889 | + "name": "Maud Poulin", | |
| 7890 | + "handles": { | |
| 7891 | + "instagram": null, | |
| 7892 | + "tiktok": null, | |
| 7893 | + "youtube": null, | |
| 7894 | + "facebook": null, | |
| 7895 | + "x": null, | |
| 7896 | + "twitch": null | |
| 7897 | + }, | |
| 7898 | + "metrics": { | |
| 7899 | + "instagram_followers": null, | |
| 7900 | + "tiktok_followers": null, | |
| 7901 | + "youtube_subscribers": null, | |
| 7902 | + "facebook_followers": null, | |
| 7903 | + "x_followers": null, | |
| 7904 | + "twitch_followers": null | |
| 7905 | + }, | |
| 7906 | + "category": "lifestyle", | |
| 7907 | + "location": "Québec", | |
| 7908 | + "notes": "Influenceuse québécoise mentionnée dans un article connexe de Billie (aucune métrique dans la source).", | |
| 7909 | + "sources": [ | |
| 7910 | + "billie_youtube" | |
| 7911 | + ], | |
| 7912 | + "total_followers": null, | |
| 7913 | + "platform_count": 0, | |
| 7914 | + "tier": "nano/inconnu" | |
| 7915 | + }, | |
| 7916 | + { | |
| 7917 | + "name": "WatchMojo Français", | |
| 7918 | + "handles": { | |
| 7919 | + "instagram": null, | |
| 7920 | + "tiktok": null, | |
| 7921 | + "youtube": "watchmojofr", | |
| 7922 | + "facebook": null, | |
| 7923 | + "x": null, | |
| 7924 | + "twitch": null | |
| 7925 | + }, | |
| 7926 | + "metrics": { | |
| 7927 | + "instagram_followers": null, | |
| 7928 | + "tiktok_followers": null, | |
| 7929 | + "youtube_subscribers": null, | |
| 7930 | + "facebook_followers": null, | |
| 7931 | + "x_followers": null, | |
| 7932 | + "twitch_followers": null | |
| 7933 | + }, | |
| 7934 | + "category": "autre", | |
| 7935 | + "location": "Québec", | |
| 7936 | + "notes": "Divertissement (tops/classements); #2 du top 100 YouTubers QC Titan, chiffres 2022 (classement par vues moyennes mensuelles, abonnés non précisés).", | |
| 7937 | + "sources": [ | |
| 7938 | + "titan_youtube_2022" | |
| 7939 | + ], | |
| 7940 | + "total_followers": null, | |
| 7941 | + "platform_count": 1, | |
| 7942 | + "tier": "nano/inconnu" | |
| 7943 | + }, | |
| 7944 | + { | |
| 7945 | + "name": "PoloBleu", | |
| 7946 | + "handles": { | |
| 7947 | + "instagram": null, | |
| 7948 | + "tiktok": null, | |
| 7949 | + "youtube": "polobleu", | |
| 7950 | + "facebook": null, | |
| 7951 | + "x": null, | |
| 7952 | + "twitch": null | |
| 7953 | + }, | |
| 7954 | + "metrics": { | |
| 7955 | + "instagram_followers": null, | |
| 7956 | + "tiktok_followers": null, | |
| 7957 | + "youtube_subscribers": null, | |
| 7958 | + "facebook_followers": null, | |
| 7959 | + "x_followers": null, | |
| 7960 | + "twitch_followers": null | |
| 7961 | + }, | |
| 7962 | + "category": "gaming", | |
| 7963 | + "location": "Québec", | |
| 7964 | + "notes": "Deuxième chaîne gaming de Polo; #3 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 7965 | + "sources": [ | |
| 7966 | + "titan_youtube_2022" | |
| 7967 | + ], | |
| 7968 | + "total_followers": null, | |
| 7969 | + "platform_count": 1, | |
| 7970 | + "tier": "nano/inconnu" | |
| 7971 | + }, | |
| 7972 | + { | |
| 7973 | + "name": "Têtes à claques", | |
| 7974 | + "handles": { | |
| 7975 | + "instagram": null, | |
| 7976 | + "tiktok": null, | |
| 7977 | + "youtube": "salamboproductions", | |
| 7978 | + "facebook": null, | |
| 7979 | + "x": null, | |
| 7980 | + "twitch": null | |
| 7981 | + }, | |
| 7982 | + "metrics": { | |
| 7983 | + "instagram_followers": null, | |
| 7984 | + "tiktok_followers": null, | |
| 7985 | + "youtube_subscribers": null, | |
| 7986 | + "facebook_followers": null, | |
| 7987 | + "x_followers": null, | |
| 7988 | + "twitch_followers": null | |
| 7989 | + }, | |
| 7990 | + "category": "humour", | |
| 7991 | + "location": "Québec", | |
| 7992 | + "notes": "Chaîne humoristique culte; #5 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 7993 | + "sources": [ | |
| 7994 | + "titan_youtube_2022" | |
| 7995 | + ], | |
| 7996 | + "total_followers": null, | |
| 7997 | + "platform_count": 1, | |
| 7998 | + "tier": "nano/inconnu" | |
| 7999 | + }, | |
| 8000 | + { | |
| 8001 | + "name": "Steelorse", | |
| 8002 | + "handles": { | |
| 8003 | + "instagram": null, | |
| 8004 | + "tiktok": null, | |
| 8005 | + "youtube": "steelorse", | |
| 8006 | + "facebook": null, | |
| 8007 | + "x": null, | |
| 8008 | + "twitch": null | |
| 8009 | + }, | |
| 8010 | + "metrics": { | |
| 8011 | + "instagram_followers": null, | |
| 8012 | + "tiktok_followers": null, | |
| 8013 | + "youtube_subscribers": null, | |
| 8014 | + "facebook_followers": null, | |
| 8015 | + "x_followers": null, | |
| 8016 | + "twitch_followers": null | |
| 8017 | + }, | |
| 8018 | + "category": "gaming", | |
| 8019 | + "location": "Québec", | |
| 8020 | + "notes": "Gaming; #6 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8021 | + "sources": [ | |
| 8022 | + "titan_youtube_2022" | |
| 8023 | + ], | |
| 8024 | + "total_followers": null, | |
| 8025 | + "platform_count": 1, | |
| 8026 | + "tier": "nano/inconnu" | |
| 8027 | + }, | |
| 8028 | + { | |
| 8029 | + "name": "Raynold Boudreau", | |
| 8030 | + "handles": { | |
| 8031 | + "instagram": null, | |
| 8032 | + "tiktok": null, | |
| 8033 | + "youtube": "boudreaurj", | |
| 8034 | + "facebook": null, | |
| 8035 | + "x": null, | |
| 8036 | + "twitch": null | |
| 8037 | + }, | |
| 8038 | + "metrics": { | |
| 8039 | + "instagram_followers": null, | |
| 8040 | + "tiktok_followers": null, | |
| 8041 | + "youtube_subscribers": null, | |
| 8042 | + "facebook_followers": null, | |
| 8043 | + "x_followers": null, | |
| 8044 | + "twitch_followers": null | |
| 8045 | + }, | |
| 8046 | + "category": "autre", | |
| 8047 | + "location": "Québec", | |
| 8048 | + "notes": "Contenu religieux; #7 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8049 | + "sources": [ | |
| 8050 | + "titan_youtube_2022" | |
| 8051 | + ], | |
| 8052 | + "total_followers": null, | |
| 8053 | + "platform_count": 1, | |
| 8054 | + "tier": "nano/inconnu" | |
| 8055 | + }, | |
| 8056 | + { | |
| 8057 | + "name": "Chef Michel Dumas", | |
| 8058 | + "handles": { | |
| 8059 | + "instagram": null, | |
| 8060 | + "tiktok": null, | |
| 8061 | + "youtube": "chefmicheldumas", | |
| 8062 | + "facebook": null, | |
| 8063 | + "x": null, | |
| 8064 | + "twitch": null | |
| 8065 | + }, | |
| 8066 | + "metrics": { | |
| 8067 | + "instagram_followers": null, | |
| 8068 | + "tiktok_followers": null, | |
| 8069 | + "youtube_subscribers": null, | |
| 8070 | + "facebook_followers": null, | |
| 8071 | + "x_followers": null, | |
| 8072 | + "twitch_followers": null | |
| 8073 | + }, | |
| 8074 | + "category": "food", | |
| 8075 | + "location": "Québec", | |
| 8076 | + "notes": "Cuisine; #10 du top 100 Titan, chiffres 2022 (abonnés non précisés). | Chef franco-canadien, vidéos de cuisine humoristiques sur TikTok/YouTube; boutique en ligne de produits dérivés. 2M abonnés cumulés selon Favikon (#12).", | |
| 8077 | + "sources": [ | |
| 8078 | + "titan_youtube_2022", | |
| 8079 | + "favikon_qc" | |
| 8080 | + ], | |
| 8081 | + "total_followers": null, | |
| 8082 | + "platform_count": 1, | |
| 8083 | + "tier": "nano/inconnu" | |
| 8084 | + }, | |
| 8085 | + { | |
| 8086 | + "name": "Cyril Chauquet", | |
| 8087 | + "handles": { | |
| 8088 | + "instagram": null, | |
| 8089 | + "tiktok": null, | |
| 8090 | + "youtube": "cyrilchauquettv", | |
| 8091 | + "facebook": null, | |
| 8092 | + "x": null, | |
| 8093 | + "twitch": null | |
| 8094 | + }, | |
| 8095 | + "metrics": { | |
| 8096 | + "instagram_followers": null, | |
| 8097 | + "tiktok_followers": null, | |
| 8098 | + "youtube_subscribers": null, | |
| 8099 | + "facebook_followers": null, | |
| 8100 | + "x_followers": null, | |
| 8101 | + "twitch_followers": null | |
| 8102 | + }, | |
| 8103 | + "category": "autre", | |
| 8104 | + "location": "Québec", | |
| 8105 | + "notes": "Pêche (Mordu de la pêche); #11 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8106 | + "sources": [ | |
| 8107 | + "titan_youtube_2022" | |
| 8108 | + ], | |
| 8109 | + "total_followers": null, | |
| 8110 | + "platform_count": 1, | |
| 8111 | + "tier": "nano/inconnu" | |
| 8112 | + }, | |
| 8113 | + { | |
| 8114 | + "name": "Loud Officiel", | |
| 8115 | + "handles": { | |
| 8116 | + "instagram": null, | |
| 8117 | + "tiktok": null, | |
| 8118 | + "youtube": "loud officiel", | |
| 8119 | + "facebook": null, | |
| 8120 | + "x": null, | |
| 8121 | + "twitch": null | |
| 8122 | + }, | |
| 8123 | + "metrics": { | |
| 8124 | + "instagram_followers": null, | |
| 8125 | + "tiktok_followers": null, | |
| 8126 | + "youtube_subscribers": null, | |
| 8127 | + "facebook_followers": null, | |
| 8128 | + "x_followers": null, | |
| 8129 | + "twitch_followers": null | |
| 8130 | + }, | |
| 8131 | + "category": "musique", | |
| 8132 | + "location": "Québec", | |
| 8133 | + "notes": "Rappeur québécois (chaîne officielle); #15 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8134 | + "sources": [ | |
| 8135 | + "titan_youtube_2022" | |
| 8136 | + ], | |
| 8137 | + "total_followers": null, | |
| 8138 | + "platform_count": 1, | |
| 8139 | + "tier": "nano/inconnu" | |
| 8140 | + }, | |
| 8141 | + { | |
| 8142 | + "name": "Light Pollux", | |
| 8143 | + "handles": { | |
| 8144 | + "instagram": null, | |
| 8145 | + "tiktok": null, | |
| 8146 | + "youtube": "light pollux", | |
| 8147 | + "facebook": null, | |
| 8148 | + "x": null, | |
| 8149 | + "twitch": null | |
| 8150 | + }, | |
| 8151 | + "metrics": { | |
| 8152 | + "instagram_followers": null, | |
| 8153 | + "tiktok_followers": null, | |
| 8154 | + "youtube_subscribers": null, | |
| 8155 | + "facebook_followers": null, | |
| 8156 | + "x_followers": null, | |
| 8157 | + "twitch_followers": null | |
| 8158 | + }, | |
| 8159 | + "category": "gaming", | |
| 8160 | + "location": "Québec", | |
| 8161 | + "notes": "Gaming; #26 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8162 | + "sources": [ | |
| 8163 | + "titan_youtube_2022" | |
| 8164 | + ], | |
| 8165 | + "total_followers": null, | |
| 8166 | + "platform_count": 1, | |
| 8167 | + "tier": "nano/inconnu" | |
| 8168 | + }, | |
| 8169 | + { | |
| 8170 | + "name": "Cath & Jay", | |
| 8171 | + "handles": { | |
| 8172 | + "instagram": null, | |
| 8173 | + "tiktok": null, | |
| 8174 | + "youtube": "cath & jay", | |
| 8175 | + "facebook": null, | |
| 8176 | + "x": null, | |
| 8177 | + "twitch": null | |
| 8178 | + }, | |
| 8179 | + "metrics": { | |
| 8180 | + "instagram_followers": null, | |
| 8181 | + "tiktok_followers": null, | |
| 8182 | + "youtube_subscribers": null, | |
| 8183 | + "facebook_followers": null, | |
| 8184 | + "x_followers": null, | |
| 8185 | + "twitch_followers": null | |
| 8186 | + }, | |
| 8187 | + "category": "lifestyle", | |
| 8188 | + "location": "Québec", | |
| 8189 | + "notes": "Duo vlog; #19 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8190 | + "sources": [ | |
| 8191 | + "titan_youtube_2022" | |
| 8192 | + ], | |
| 8193 | + "total_followers": null, | |
| 8194 | + "platform_count": 1, | |
| 8195 | + "tier": "nano/inconnu" | |
| 8196 | + }, | |
| 8197 | + { | |
| 8198 | + "name": "Gilles pis la p'tite", | |
| 8199 | + "handles": { | |
| 8200 | + "instagram": null, | |
| 8201 | + "tiktok": null, | |
| 8202 | + "youtube": "gilles pis la p'tite", | |
| 8203 | + "facebook": null, | |
| 8204 | + "x": null, | |
| 8205 | + "twitch": null | |
| 8206 | + }, | |
| 8207 | + "metrics": { | |
| 8208 | + "instagram_followers": null, | |
| 8209 | + "tiktok_followers": null, | |
| 8210 | + "youtube_subscribers": null, | |
| 8211 | + "facebook_followers": null, | |
| 8212 | + "x_followers": null, | |
| 8213 | + "twitch_followers": null | |
| 8214 | + }, | |
| 8215 | + "category": "humour", | |
| 8216 | + "location": "Québec", | |
| 8217 | + "notes": "Humour; #40 du top 100 Titan (nouvellement sondé), chiffres 2022 (abonnés non précisés).", | |
| 8218 | + "sources": [ | |
| 8219 | + "titan_youtube_2022" | |
| 8220 | + ], | |
| 8221 | + "total_followers": null, | |
| 8222 | + "platform_count": 1, | |
| 8223 | + "tier": "nano/inconnu" | |
| 8224 | + }, | |
| 8225 | + { | |
| 8226 | + "name": "AndrewTche", | |
| 8227 | + "handles": { | |
| 8228 | + "instagram": null, | |
| 8229 | + "tiktok": null, | |
| 8230 | + "youtube": "andrewtche", | |
| 8231 | + "facebook": null, | |
| 8232 | + "x": null, | |
| 8233 | + "twitch": null | |
| 8234 | + }, | |
| 8235 | + "metrics": { | |
| 8236 | + "instagram_followers": null, | |
| 8237 | + "tiktok_followers": null, | |
| 8238 | + "youtube_subscribers": null, | |
| 8239 | + "facebook_followers": null, | |
| 8240 | + "x_followers": null, | |
| 8241 | + "twitch_followers": null | |
| 8242 | + }, | |
| 8243 | + "category": "humour", | |
| 8244 | + "location": "Québec", | |
| 8245 | + "notes": "Humour; #42 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8246 | + "sources": [ | |
| 8247 | + "titan_youtube_2022" | |
| 8248 | + ], | |
| 8249 | + "total_followers": null, | |
| 8250 | + "platform_count": 1, | |
| 8251 | + "tier": "nano/inconnu" | |
| 8252 | + }, | |
| 8253 | + { | |
| 8254 | + "name": "Chest Bras", | |
| 8255 | + "handles": { | |
| 8256 | + "instagram": null, | |
| 8257 | + "tiktok": null, | |
| 8258 | + "youtube": "chest bras", | |
| 8259 | + "facebook": null, | |
| 8260 | + "x": null, | |
| 8261 | + "twitch": null | |
| 8262 | + }, | |
| 8263 | + "metrics": { | |
| 8264 | + "instagram_followers": null, | |
| 8265 | + "tiktok_followers": null, | |
| 8266 | + "youtube_subscribers": null, | |
| 8267 | + "facebook_followers": null, | |
| 8268 | + "x_followers": null, | |
| 8269 | + "twitch_followers": null | |
| 8270 | + }, | |
| 8271 | + "category": "humour", | |
| 8272 | + "location": "Québec", | |
| 8273 | + "notes": "Humour; #44 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8274 | + "sources": [ | |
| 8275 | + "titan_youtube_2022" | |
| 8276 | + ], | |
| 8277 | + "total_followers": null, | |
| 8278 | + "platform_count": 1, | |
| 8279 | + "tier": "nano/inconnu" | |
| 8280 | + }, | |
| 8281 | + { | |
| 8282 | + "name": "2 Filles Ordinaires", | |
| 8283 | + "handles": { | |
| 8284 | + "instagram": null, | |
| 8285 | + "tiktok": null, | |
| 8286 | + "youtube": "2 filles ordinaires", | |
| 8287 | + "facebook": null, | |
| 8288 | + "x": null, | |
| 8289 | + "twitch": null | |
| 8290 | + }, | |
| 8291 | + "metrics": { | |
| 8292 | + "instagram_followers": null, | |
| 8293 | + "tiktok_followers": null, | |
| 8294 | + "youtube_subscribers": null, | |
| 8295 | + "facebook_followers": null, | |
| 8296 | + "x_followers": null, | |
| 8297 | + "twitch_followers": null | |
| 8298 | + }, | |
| 8299 | + "category": "beauté", | |
| 8300 | + "location": "Québec", | |
| 8301 | + "notes": "Beauté et mode; #46 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8302 | + "sources": [ | |
| 8303 | + "titan_youtube_2022" | |
| 8304 | + ], | |
| 8305 | + "total_followers": null, | |
| 8306 | + "platform_count": 1, | |
| 8307 | + "tier": "nano/inconnu" | |
| 8308 | + }, | |
| 8309 | + { | |
| 8310 | + "name": "Billie", | |
| 8311 | + "handles": { | |
| 8312 | + "instagram": null, | |
| 8313 | + "tiktok": null, | |
| 8314 | + "youtube": "billie", | |
| 8315 | + "facebook": null, | |
| 8316 | + "x": null, | |
| 8317 | + "twitch": null | |
| 8318 | + }, | |
| 8319 | + "metrics": { | |
| 8320 | + "instagram_followers": null, | |
| 8321 | + "tiktok_followers": null, | |
| 8322 | + "youtube_subscribers": null, | |
| 8323 | + "facebook_followers": null, | |
| 8324 | + "x_followers": null, | |
| 8325 | + "twitch_followers": null | |
| 8326 | + }, | |
| 8327 | + "category": "beauté", | |
| 8328 | + "location": "Québec", | |
| 8329 | + "notes": "Chaîne beauté/mode (Québecor); #48 du top 100 Titan, chiffres 2022 (abonnés non précisés).", | |
| 8330 | + "sources": [ | |
| 8331 | + "titan_youtube_2022" | |
| 8332 | + ], | |
| 8333 | + "total_followers": null, | |
| 8334 | + "platform_count": 1, | |
| 8335 | + "tier": "nano/inconnu" | |
| 8336 | + }, | |
| 8337 | + { | |
| 8338 | + "name": "Hélène Boudreau", | |
| 8339 | + "handles": { | |
| 8340 | + "instagram": null, | |
| 8341 | + "tiktok": null, | |
| 8342 | + "youtube": null, | |
| 8343 | + "facebook": null, | |
| 8344 | + "x": null, | |
| 8345 | + "twitch": null | |
| 8346 | + }, | |
| 8347 | + "metrics": { | |
| 8348 | + "instagram_followers": null, | |
| 8349 | + "tiktok_followers": null, | |
| 8350 | + "youtube_subscribers": null, | |
| 8351 | + "facebook_followers": null, | |
| 8352 | + "x_followers": null, | |
| 8353 | + "twitch_followers": null | |
| 8354 | + }, | |
| 8355 | + "category": "autre", | |
| 8356 | + "location": "Québec", | |
| 8357 | + "notes": "Créatrice de contenu adulte (OnlyFans), #1 du top créatrices adultes du Québec selon canadult; visibilité internationale (USA + Canada), revenus estimés 1M$–3M$+.", | |
| 8358 | + "sources": [ | |
| 8359 | + "canadult" | |
| 8360 | + ], | |
| 8361 | + "total_followers": null, | |
| 8362 | + "platform_count": 0, | |
| 8363 | + "tier": "nano/inconnu" | |
| 8364 | + }, | |
| 8365 | + { | |
| 8366 | + "name": "Jade Lavoie", | |
| 8367 | + "handles": { | |
| 8368 | + "instagram": null, | |
| 8369 | + "tiktok": null, | |
| 8370 | + "youtube": null, | |
| 8371 | + "facebook": null, | |
| 8372 | + "x": null, | |
| 8373 | + "twitch": null | |
| 8374 | + }, | |
| 8375 | + "metrics": { | |
| 8376 | + "instagram_followers": null, | |
| 8377 | + "tiktok_followers": null, | |
| 8378 | + "youtube_subscribers": null, | |
| 8379 | + "facebook_followers": null, | |
| 8380 | + "x_followers": null, | |
| 8381 | + "twitch_followers": null | |
| 8382 | + }, | |
| 8383 | + "category": "autre", | |
| 8384 | + "location": "Québec", | |
| 8385 | + "notes": "Créatrice de contenu adulte (OnlyFans), star adulte internationale selon canadult; revenus estimés 1M$+.", | |
| 8386 | + "sources": [ | |
| 8387 | + "canadult" | |
| 8388 | + ], | |
| 8389 | + "total_followers": null, | |
| 8390 | + "platform_count": 0, | |
| 8391 | + "tier": "nano/inconnu" | |
| 8392 | + }, | |
| 8393 | + { | |
| 8394 | + "name": "Aïcha Black", | |
| 8395 | + "handles": { | |
| 8396 | + "instagram": null, | |
| 8397 | + "tiktok": null, | |
| 8398 | + "youtube": null, | |
| 8399 | + "facebook": null, | |
| 8400 | + "x": null, | |
| 8401 | + "twitch": null | |
| 8402 | + }, | |
| 8403 | + "metrics": { | |
| 8404 | + "instagram_followers": null, | |
| 8405 | + "tiktok_followers": null, | |
| 8406 | + "youtube_subscribers": null, | |
| 8407 | + "facebook_followers": null, | |
| 8408 | + "x_followers": null, | |
| 8409 | + "twitch_followers": null | |
| 8410 | + }, | |
| 8411 | + "category": "autre", | |
| 8412 | + "location": "Québec", | |
| 8413 | + "notes": "Créatrice de contenu adulte (OnlyFans), niche premium; audience Québec + France, revenus estimés 100k$–500k$ (canadult).", | |
| 8414 | + "sources": [ | |
| 8415 | + "canadult" | |
| 8416 | + ], | |
| 8417 | + "total_followers": null, | |
| 8418 | + "platform_count": 0, | |
| 8419 | + "tier": "nano/inconnu" | |
| 8420 | + }, | |
| 8421 | + { | |
| 8422 | + "name": "Eva Prevost", | |
| 8423 | + "handles": { | |
| 8424 | + "instagram": null, | |
| 8425 | + "tiktok": null, | |
| 8426 | + "youtube": null, | |
| 8427 | + "facebook": null, | |
| 8428 | + "x": null, | |
| 8429 | + "twitch": null | |
| 8430 | + }, | |
| 8431 | + "metrics": { | |
| 8432 | + "instagram_followers": null, | |
| 8433 | + "tiktok_followers": null, | |
| 8434 | + "youtube_subscribers": null, | |
| 8435 | + "facebook_followers": null, | |
| 8436 | + "x_followers": null, | |
| 8437 | + "twitch_followers": null | |
| 8438 | + }, | |
| 8439 | + "category": "autre", | |
| 8440 | + "location": "Québec", | |
| 8441 | + "notes": "Créatrice de contenu adulte (OnlyFans) en forte croissance sociale; audience Canada francophone, revenus estimés 100k$–500k$ (canadult).", | |
| 8442 | + "sources": [ | |
| 8443 | + "canadult" | |
| 8444 | + ], | |
| 8445 | + "total_followers": null, | |
| 8446 | + "platform_count": 0, | |
| 8447 | + "tier": "nano/inconnu" | |
| 8448 | + }, | |
| 8449 | + { | |
| 8450 | + "name": "Peachjars", | |
| 8451 | + "handles": { | |
| 8452 | + "instagram": null, | |
| 8453 | + "tiktok": null, | |
| 8454 | + "youtube": null, | |
| 8455 | + "facebook": null, | |
| 8456 | + "x": null, | |
| 8457 | + "twitch": null | |
| 8458 | + }, | |
| 8459 | + "metrics": { | |
| 8460 | + "instagram_followers": null, | |
| 8461 | + "tiktok_followers": null, | |
| 8462 | + "youtube_subscribers": null, | |
| 8463 | + "facebook_followers": null, | |
| 8464 | + "x_followers": null, | |
| 8465 | + "twitch_followers": null | |
| 8466 | + }, | |
| 8467 | + "category": "autre", | |
| 8468 | + "location": "Québec", | |
| 8469 | + "notes": "Créatrice gaming + contenu adulte (OnlyFans), audience internationale, revenus estimés 500k$+ (canadult).", | |
| 8470 | + "sources": [ | |
| 8471 | + "canadult" | |
| 8472 | + ], | |
| 8473 | + "total_followers": null, | |
| 8474 | + "platform_count": 0, | |
| 8475 | + "tier": "nano/inconnu" | |
| 8476 | + }, | |
| 8477 | + { | |
| 8478 | + "name": "Cindy Cournoyer", | |
| 8479 | + "handles": { | |
| 8480 | + "instagram": null, | |
| 8481 | + "tiktok": null, | |
| 8482 | + "youtube": null, | |
| 8483 | + "facebook": null, | |
| 8484 | + "x": null, | |
| 8485 | + "twitch": null | |
| 8486 | + }, | |
| 8487 | + "metrics": { | |
| 8488 | + "instagram_followers": null, | |
| 8489 | + "tiktok_followers": null, | |
| 8490 | + "youtube_subscribers": null, | |
| 8491 | + "facebook_followers": null, | |
| 8492 | + "x_followers": null, | |
| 8493 | + "twitch_followers": null | |
| 8494 | + }, | |
| 8495 | + "category": "autre", | |
| 8496 | + "location": "Québec", | |
| 8497 | + "notes": "Créatrice lifestyle + contenu premium (OnlyFans); revenus estimés 100k$–300k$ (canadult).", | |
| 8498 | + "sources": [ | |
| 8499 | + "canadult" | |
| 8500 | + ], | |
| 8501 | + "total_followers": null, | |
| 8502 | + "platform_count": 0, | |
| 8503 | + "tier": "nano/inconnu" | |
| 8504 | + }, | |
| 8505 | + { | |
| 8506 | + "name": "Xavier Dolan", | |
| 8507 | + "handles": { | |
| 8508 | + "instagram": null, | |
| 8509 | + "tiktok": null, | |
| 8510 | + "youtube": null, | |
| 8511 | + "facebook": null, | |
| 8512 | + "x": null, | |
| 8513 | + "twitch": null | |
| 8514 | + }, | |
| 8515 | + "metrics": { | |
| 8516 | + "instagram_followers": null, | |
| 8517 | + "tiktok_followers": null, | |
| 8518 | + "youtube_subscribers": null, | |
| 8519 | + "facebook_followers": null, | |
| 8520 | + "x_followers": null, | |
| 8521 | + "twitch_followers": null | |
| 8522 | + }, | |
| 8523 | + "category": "autre", | |
| 8524 | + "location": "Montréal", | |
| 8525 | + "notes": "Cinéaste cité par canadult comme exemple de personnalité québécoise au rayonnement international; pas un influenceur des réseaux sociaux au sens strict. | Cinéaste; 8e du top Twitter QC 2018, a quitté Twitter en 2018", | |
| 8526 | + "sources": [ | |
| 8527 | + "canadult", | |
| 8528 | + "rythme_twitter" | |
| 8529 | + ], | |
| 8530 | + "total_followers": null, | |
| 8531 | + "platform_count": 0, | |
| 8532 | + "tier": "nano/inconnu" | |
| 8533 | + }, | |
| 8534 | + { | |
| 8535 | + "name": "Farnell Morisset", | |
| 8536 | + "handles": { | |
| 8537 | + "instagram": null, | |
| 8538 | + "tiktok": null, | |
| 8539 | + "youtube": null, | |
| 8540 | + "facebook": null, | |
| 8541 | + "x": null, | |
| 8542 | + "twitch": null | |
| 8543 | + }, | |
| 8544 | + "metrics": { | |
| 8545 | + "instagram_followers": null, | |
| 8546 | + "tiktok_followers": null, | |
| 8547 | + "youtube_subscribers": null, | |
| 8548 | + "facebook_followers": null, | |
| 8549 | + "x_followers": null, | |
| 8550 | + "twitch_followers": null | |
| 8551 | + }, | |
| 8552 | + "category": "autre", | |
| 8553 | + "location": "Québec", | |
| 8554 | + "notes": "Gagnant Gala InfluenceCréation 2025 — Prix hommage, influence positive et rayonnement social.", | |
| 8555 | + "sources": [ | |
| 8556 | + "billie_gala", | |
| 8557 | + "grenier_gala" | |
| 8558 | + ], | |
| 8559 | + "total_followers": null, | |
| 8560 | + "platform_count": 0, | |
| 8561 | + "tier": "nano/inconnu" | |
| 8562 | + }, | |
| 8563 | + { | |
| 8564 | + "name": "Laura L. Marchand", | |
| 8565 | + "handles": { | |
| 8566 | + "instagram": "laulauthecoconut", | |
| 8567 | + "tiktok": null, | |
| 8568 | + "youtube": null, | |
| 8569 | + "facebook": null, | |
| 8570 | + "x": null, | |
| 8571 | + "twitch": null | |
| 8572 | + }, | |
| 8573 | + "metrics": { | |
| 8574 | + "instagram_followers": null, | |
| 8575 | + "tiktok_followers": null, | |
| 8576 | + "youtube_subscribers": null, | |
| 8577 | + "facebook_followers": null, | |
| 8578 | + "x_followers": null, | |
| 8579 | + "twitch_followers": null | |
| 8580 | + }, | |
| 8581 | + "category": "autre", | |
| 8582 | + "location": "Québec", | |
| 8583 | + "notes": "Gagnante Gala InfluenceCréation 2025 — Découverte de l'année (@laulauthecoconut).", | |
| 8584 | + "sources": [ | |
| 8585 | + "billie_gala", | |
| 8586 | + "grenier_gala" | |
| 8587 | + ], | |
| 8588 | + "total_followers": null, | |
| 8589 | + "platform_count": 1, | |
| 8590 | + "tier": "nano/inconnu" | |
| 8591 | + }, | |
| 8592 | + { | |
| 8593 | + "name": "Annie Archambault", | |
| 8594 | + "handles": { | |
| 8595 | + "instagram": "surleborddelaligne", | |
| 8596 | + "tiktok": null, | |
| 8597 | + "youtube": null, | |
| 8598 | + "facebook": null, | |
| 8599 | + "x": null, | |
| 8600 | + "twitch": null | |
| 8601 | + }, | |
| 8602 | + "metrics": { | |
| 8603 | + "instagram_followers": null, | |
| 8604 | + "tiktok_followers": null, | |
| 8605 | + "youtube_subscribers": null, | |
| 8606 | + "facebook_followers": null, | |
| 8607 | + "x_followers": null, | |
| 8608 | + "twitch_followers": null | |
| 8609 | + }, | |
| 8610 | + "category": "autre", | |
| 8611 | + "location": "Québec", | |
| 8612 | + "notes": "Gagnante Gala InfluenceCréation 2025 — Sensibilisation (@surleborddelaligne).", | |
| 8613 | + "sources": [ | |
| 8614 | + "billie_gala", | |
| 8615 | + "grenier_gala" | |
| 8616 | + ], | |
| 8617 | + "total_followers": null, | |
| 8618 | + "platform_count": 1, | |
| 8619 | + "tier": "nano/inconnu" | |
| 8620 | + }, | |
| 8621 | + { | |
| 8622 | + "name": "Vicky Powell", | |
| 8623 | + "handles": { | |
| 8624 | + "instagram": "vp.law", | |
| 8625 | + "tiktok": null, | |
| 8626 | + "youtube": null, | |
| 8627 | + "facebook": null, | |
| 8628 | + "x": null, | |
| 8629 | + "twitch": null | |
| 8630 | + }, | |
| 8631 | + "metrics": { | |
| 8632 | + "instagram_followers": null, | |
| 8633 | + "tiktok_followers": null, | |
| 8634 | + "youtube_subscribers": null, | |
| 8635 | + "facebook_followers": null, | |
| 8636 | + "x_followers": null, | |
| 8637 | + "twitch_followers": null | |
| 8638 | + }, | |
| 8639 | + "category": "autre", | |
| 8640 | + "location": "Québec", | |
| 8641 | + "notes": "Gagnante Gala InfluenceCréation 2025 — Éducation (@vp.law, vulgarisation juridique).", | |
| 8642 | + "sources": [ | |
| 8643 | + "billie_gala", | |
| 8644 | + "grenier_gala" | |
| 8645 | + ], | |
| 8646 | + "total_followers": null, | |
| 8647 | + "platform_count": 1, | |
| 8648 | + "tier": "nano/inconnu" | |
| 8649 | + }, | |
| 8650 | + { | |
| 8651 | + "name": "Laurent Turcot", | |
| 8652 | + "handles": { | |
| 8653 | + "instagram": null, | |
| 8654 | + "tiktok": null, | |
| 8655 | + "youtube": null, | |
| 8656 | + "facebook": null, | |
| 8657 | + "x": null, | |
| 8658 | + "twitch": null | |
| 8659 | + }, | |
| 8660 | + "metrics": { | |
| 8661 | + "instagram_followers": null, | |
| 8662 | + "tiktok_followers": null, | |
| 8663 | + "youtube_subscribers": null, | |
| 8664 | + "facebook_followers": null, | |
| 8665 | + "x_followers": null, | |
| 8666 | + "twitch_followers": null | |
| 8667 | + }, | |
| 8668 | + "category": "autre", | |
| 8669 | + "location": "Québec", | |
| 8670 | + "notes": "Historien derrière la chaîne L'Histoire nous le dira; gagnant Gala InfluenceCréation 2025 — Vidéo de l'année, format court.", | |
| 8671 | + "sources": [ | |
| 8672 | + "billie_gala", | |
| 8673 | + "grenier_gala" | |
| 8674 | + ], | |
| 8675 | + "total_followers": null, | |
| 8676 | + "platform_count": 0, | |
| 8677 | + "tier": "nano/inconnu" | |
| 8678 | + }, | |
| 8679 | + { | |
| 8680 | + "name": "Geneviève O'Gleman", | |
| 8681 | + "handles": { | |
| 8682 | + "instagram": null, | |
| 8683 | + "tiktok": null, | |
| 8684 | + "youtube": null, | |
| 8685 | + "facebook": null, | |
| 8686 | + "x": null, | |
| 8687 | + "twitch": null | |
| 8688 | + }, | |
| 8689 | + "metrics": { | |
| 8690 | + "instagram_followers": null, | |
| 8691 | + "tiktok_followers": null, | |
| 8692 | + "youtube_subscribers": null, | |
| 8693 | + "facebook_followers": null, | |
| 8694 | + "x_followers": null, | |
| 8695 | + "twitch_followers": null | |
| 8696 | + }, | |
| 8697 | + "category": "food", | |
| 8698 | + "location": "Québec", | |
| 8699 | + "notes": "Nutritionniste et créatrice culinaire; gagnante Gala InfluenceCréation 2025 — Chef, foodie & nutrition.", | |
| 8700 | + "sources": [ | |
| 8701 | + "billie_gala", | |
| 8702 | + "grenier_gala" | |
| 8703 | + ], | |
| 8704 | + "total_followers": null, | |
| 8705 | + "platform_count": 0, | |
| 8706 | + "tier": "nano/inconnu" | |
| 8707 | + }, | |
| 8708 | + { | |
| 8709 | + "name": "Félix Daigle", | |
| 8710 | + "handles": { | |
| 8711 | + "instagram": null, | |
| 8712 | + "tiktok": null, | |
| 8713 | + "youtube": null, | |
| 8714 | + "facebook": null, | |
| 8715 | + "x": null, | |
| 8716 | + "twitch": null | |
| 8717 | + }, | |
| 8718 | + "metrics": { | |
| 8719 | + "instagram_followers": null, | |
| 8720 | + "tiktok_followers": null, | |
| 8721 | + "youtube_subscribers": null, | |
| 8722 | + "facebook_followers": null, | |
| 8723 | + "x_followers": null, | |
| 8724 | + "twitch_followers": null | |
| 8725 | + }, | |
| 8726 | + "category": "fitness", | |
| 8727 | + "location": "Québec", | |
| 8728 | + "notes": "Gagnant Gala InfluenceCréation 2025 — Sport & fitness.", | |
| 8729 | + "sources": [ | |
| 8730 | + "billie_gala", | |
| 8731 | + "grenier_gala" | |
| 8732 | + ], | |
| 8733 | + "total_followers": null, | |
| 8734 | + "platform_count": 0, | |
| 8735 | + "tier": "nano/inconnu" | |
| 8736 | + }, | |
| 8737 | + { | |
| 8738 | + "name": "Sarah Hamel", | |
| 8739 | + "handles": { | |
| 8740 | + "instagram": null, | |
| 8741 | + "tiktok": null, | |
| 8742 | + "youtube": null, | |
| 8743 | + "facebook": null, | |
| 8744 | + "x": null, | |
| 8745 | + "twitch": null | |
| 8746 | + }, | |
| 8747 | + "metrics": { | |
| 8748 | + "instagram_followers": null, | |
| 8749 | + "tiktok_followers": null, | |
| 8750 | + "youtube_subscribers": null, | |
| 8751 | + "facebook_followers": null, | |
| 8752 | + "x_followers": null, | |
| 8753 | + "twitch_followers": null | |
| 8754 | + }, | |
| 8755 | + "category": "autre", | |
| 8756 | + "location": "Québec", | |
| 8757 | + "notes": "Gagnante Gala InfluenceCréation 2025 — Famille & parentalité.", | |
| 8758 | + "sources": [ | |
| 8759 | + "billie_gala", | |
| 8760 | + "grenier_gala" | |
| 8761 | + ], | |
| 8762 | + "total_followers": null, | |
| 8763 | + "platform_count": 0, | |
| 8764 | + "tier": "nano/inconnu" | |
| 8765 | + }, | |
| 8766 | + { | |
| 8767 | + "name": "Hellfrank", | |
| 8768 | + "handles": { | |
| 8769 | + "instagram": null, | |
| 8770 | + "tiktok": null, | |
| 8771 | + "youtube": null, | |
| 8772 | + "facebook": null, | |
| 8773 | + "x": null, | |
| 8774 | + "twitch": null | |
| 8775 | + }, | |
| 8776 | + "metrics": { | |
| 8777 | + "instagram_followers": null, | |
| 8778 | + "tiktok_followers": null, | |
| 8779 | + "youtube_subscribers": null, | |
| 8780 | + "facebook_followers": null, | |
| 8781 | + "x_followers": null, | |
| 8782 | + "twitch_followers": null | |
| 8783 | + }, | |
| 8784 | + "category": "gaming", | |
| 8785 | + "location": "Québec", | |
| 8786 | + "notes": "Gagnant Gala InfluenceCréation 2025 — Créateur émergent, catégorie Streamer.", | |
| 8787 | + "sources": [ | |
| 8788 | + "grenier_gala" | |
| 8789 | + ], | |
| 8790 | + "total_followers": null, | |
| 8791 | + "platform_count": 0, | |
| 8792 | + "tier": "nano/inconnu" | |
| 8793 | + }, | |
| 8794 | + { | |
| 8795 | + "name": "Mon bro", | |
| 8796 | + "handles": { | |
| 8797 | + "instagram": null, | |
| 8798 | + "tiktok": null, | |
| 8799 | + "youtube": null, | |
| 8800 | + "facebook": null, | |
| 8801 | + "x": null, | |
| 8802 | + "twitch": null | |
| 8803 | + }, | |
| 8804 | + "metrics": { | |
| 8805 | + "instagram_followers": null, | |
| 8806 | + "tiktok_followers": null, | |
| 8807 | + "youtube_subscribers": null, | |
| 8808 | + "facebook_followers": null, | |
| 8809 | + "x_followers": null, | |
| 8810 | + "twitch_followers": null | |
| 8811 | + }, | |
| 8812 | + "category": "autre", | |
| 8813 | + "location": "Québec", | |
| 8814 | + "notes": "Gagnant Gala InfluenceCréation 2025 — Créateur émergent, catégorie Créateur.", | |
| 8815 | + "sources": [ | |
| 8816 | + "grenier_gala" | |
| 8817 | + ], | |
| 8818 | + "total_followers": null, | |
| 8819 | + "platform_count": 0, | |
| 8820 | + "tier": "nano/inconnu" | |
| 8821 | + }, | |
| 8822 | + { | |
| 8823 | + "name": "Olivia Leclerc", | |
| 8824 | + "handles": { | |
| 8825 | + "instagram": null, | |
| 8826 | + "tiktok": null, | |
| 8827 | + "youtube": null, | |
| 8828 | + "facebook": null, | |
| 8829 | + "x": null, | |
| 8830 | + "twitch": null | |
| 8831 | + }, | |
| 8832 | + "metrics": { | |
| 8833 | + "instagram_followers": null, | |
| 8834 | + "tiktok_followers": null, | |
| 8835 | + "youtube_subscribers": null, | |
| 8836 | + "facebook_followers": null, | |
| 8837 | + "x_followers": null, | |
| 8838 | + "twitch_followers": null | |
| 8839 | + }, | |
| 8840 | + "category": "autre", | |
| 8841 | + "location": "Québec", | |
| 8842 | + "notes": "Coanimatrice du Gala InfluenceCréation 2025 (avec Jérémie Latreille / Le Fit Cook).", | |
| 8843 | + "sources": [ | |
| 8844 | + "grenier_gala" | |
| 8845 | + ], | |
| 8846 | + "total_followers": null, | |
| 8847 | + "platform_count": 0, | |
| 8848 | + "tier": "nano/inconnu" | |
| 8849 | + }, | |
| 8850 | + { | |
| 8851 | + "name": "Claude Bégin", | |
| 8852 | + "handles": { | |
| 8853 | + "instagram": null, | |
| 8854 | + "tiktok": null, | |
| 8855 | + "youtube": null, | |
| 8856 | + "facebook": null, | |
| 8857 | + "x": null, | |
| 8858 | + "twitch": null | |
| 8859 | + }, | |
| 8860 | + "metrics": { | |
| 8861 | + "instagram_followers": null, | |
| 8862 | + "tiktok_followers": null, | |
| 8863 | + "youtube_subscribers": null, | |
| 8864 | + "facebook_followers": null, | |
| 8865 | + "x_followers": null, | |
| 8866 | + "twitch_followers": null | |
| 8867 | + }, | |
| 8868 | + "category": "musique", | |
| 8869 | + "location": "Québec", | |
| 8870 | + "notes": "Musicien; coanime le balado Parenthèse avec Lysandre Nadeau — Balado de l'année, Gala InfluenceCréation 2025; coauteur d'un livre jeunesse avec elle.", | |
| 8871 | + "sources": [ | |
| 8872 | + "frank", | |
| 8873 | + "billie_gala", | |
| 8874 | + "grenier_gala" | |
| 8875 | + ], | |
| 8876 | + "total_followers": null, | |
| 8877 | + "platform_count": 0, | |
| 8878 | + "tier": "nano/inconnu" | |
| 8879 | + }, | |
| 8880 | + { | |
| 8881 | + "name": "Martin et Caroline", | |
| 8882 | + "handles": { | |
| 8883 | + "instagram": null, | |
| 8884 | + "tiktok": null, | |
| 8885 | + "youtube": null, | |
| 8886 | + "facebook": null, | |
| 8887 | + "x": null, | |
| 8888 | + "twitch": null | |
| 8889 | + }, | |
| 8890 | + "metrics": { | |
| 8891 | + "instagram_followers": null, | |
| 8892 | + "tiktok_followers": null, | |
| 8893 | + "youtube_subscribers": null, | |
| 8894 | + "facebook_followers": null, | |
| 8895 | + "x_followers": null, | |
| 8896 | + "twitch_followers": null | |
| 8897 | + }, | |
| 8898 | + "category": "humour", | |
| 8899 | + "location": "Montréal", | |
| 8900 | + "notes": "Couple franco-canadien; humour et lifestyle sur les différences culturelles France/Québec, conseils d'immigration. 1,2M abonnés cumulés selon Favikon (#20).", | |
| 8901 | + "sources": [ | |
| 8902 | + "favikon_qc" | |
| 8903 | + ], | |
| 8904 | + "total_followers": null, | |
| 8905 | + "platform_count": 0, | |
| 8906 | + "tier": "nano/inconnu" | |
| 8907 | + }, | |
| 8908 | + { | |
| 8909 | + "name": "Fredz", | |
| 8910 | + "handles": { | |
| 8911 | + "instagram": null, | |
| 8912 | + "tiktok": null, | |
| 8913 | + "youtube": null, | |
| 8914 | + "facebook": null, | |
| 8915 | + "x": null, | |
| 8916 | + "twitch": null | |
| 8917 | + }, | |
| 8918 | + "metrics": { | |
| 8919 | + "instagram_followers": null, | |
| 8920 | + "tiktok_followers": null, | |
| 8921 | + "youtube_subscribers": null, | |
| 8922 | + "facebook_followers": null, | |
| 8923 | + "x_followers": null, | |
| 8924 | + "twitch_followers": null | |
| 8925 | + }, | |
| 8926 | + "category": "musique", | |
| 8927 | + "location": "Québec", | |
| 8928 | + "notes": "Musicien et performeur (chanson « Ce soir (j'suis dans ma tête) »), en tournée au Canada. 1M abonnés cumulés selon Favikon (#19).", | |
| 8929 | + "sources": [ | |
| 8930 | + "favikon_qc" | |
| 8931 | + ], | |
| 8932 | + "total_followers": null, | |
| 8933 | + "platform_count": 0, | |
| 8934 | + "tier": "nano/inconnu" | |
| 8935 | + }, | |
| 8936 | + { | |
| 8937 | + "name": "Zac Bulle", | |
| 8938 | + "handles": { | |
| 8939 | + "instagram": null, | |
| 8940 | + "tiktok": null, | |
| 8941 | + "youtube": null, | |
| 8942 | + "facebook": null, | |
| 8943 | + "x": null, | |
| 8944 | + "twitch": null | |
| 8945 | + }, | |
| 8946 | + "metrics": { | |
| 8947 | + "instagram_followers": null, | |
| 8948 | + "tiktok_followers": null, | |
| 8949 | + "youtube_subscribers": null, | |
| 8950 | + "facebook_followers": null, | |
| 8951 | + "x_followers": null, | |
| 8952 | + "twitch_followers": null | |
| 8953 | + }, | |
| 8954 | + "category": "food", | |
| 8955 | + "location": "Québec", | |
| 8956 | + "notes": "YouTubeur connu pour ses critiques de poutine et son contenu comique. 624,7K abonnés cumulés selon Favikon (#10).", | |
| 8957 | + "sources": [ | |
| 8958 | + "favikon_qc" | |
| 8959 | + ], | |
| 8960 | + "total_followers": null, | |
| 8961 | + "platform_count": 0, | |
| 8962 | + "tier": "nano/inconnu" | |
| 8963 | + }, | |
| 8964 | + { | |
| 8965 | + "name": "Romy Victory", | |
| 8966 | + "handles": { | |
| 8967 | + "instagram": null, | |
| 8968 | + "tiktok": null, | |
| 8969 | + "youtube": null, | |
| 8970 | + "facebook": null, | |
| 8971 | + "x": null, | |
| 8972 | + "twitch": null | |
| 8973 | + }, | |
| 8974 | + "metrics": { | |
| 8975 | + "instagram_followers": null, | |
| 8976 | + "tiktok_followers": null, | |
| 8977 | + "youtube_subscribers": null, | |
| 8978 | + "facebook_followers": null, | |
| 8979 | + "x_followers": null, | |
| 8980 | + "twitch_followers": null | |
| 8981 | + }, | |
| 8982 | + "category": "mode", | |
| 8983 | + "location": "Montréal", | |
| 8984 | + "notes": "YouTubeuse mode vintage et lifestyle, aussi actrice et streameuse Twitch. 1,9M abonnés cumulés selon Favikon (#8).", | |
| 8985 | + "sources": [ | |
| 8986 | + "favikon_qc" | |
| 8987 | + ], | |
| 8988 | + "total_followers": null, | |
| 8989 | + "platform_count": 0, | |
| 8990 | + "tier": "nano/inconnu" | |
| 8991 | + }, | |
| 8992 | + { | |
| 8993 | + "name": "Zequoiza", | |
| 8994 | + "handles": { | |
| 8995 | + "instagram": null, | |
| 8996 | + "tiktok": null, | |
| 8997 | + "youtube": null, | |
| 8998 | + "facebook": null, | |
| 8999 | + "x": null, | |
| 9000 | + "twitch": null | |
| 9001 | + }, | |
| 9002 | + "metrics": { | |
| 9003 | + "instagram_followers": null, | |
| 9004 | + "tiktok_followers": null, | |
| 9005 | + "youtube_subscribers": null, | |
| 9006 | + "facebook_followers": null, | |
| 9007 | + "x_followers": null, | |
| 9008 | + "twitch_followers": null | |
| 9009 | + }, | |
| 9010 | + "category": "humour", | |
| 9011 | + "location": "Québec", | |
| 9012 | + "notes": "Personnalité humoristique des réseaux sociaux, blagues et anecdotes. 1,3M abonnés cumulés selon Favikon (#7).", | |
| 9013 | + "sources": [ | |
| 9014 | + "favikon_qc" | |
| 9015 | + ], | |
| 9016 | + "total_followers": null, | |
| 9017 | + "platform_count": 0, | |
| 9018 | + "tier": "nano/inconnu" | |
| 9019 | + }, | |
| 9020 | + { | |
| 9021 | + "name": "Redazere", | |
| 9022 | + "handles": { | |
| 9023 | + "instagram": null, | |
| 9024 | + "tiktok": null, | |
| 9025 | + "youtube": null, | |
| 9026 | + "facebook": null, | |
| 9027 | + "x": null, | |
| 9028 | + "twitch": null | |
| 9029 | + }, | |
| 9030 | + "metrics": { | |
| 9031 | + "instagram_followers": null, | |
| 9032 | + "tiktok_followers": null, | |
| 9033 | + "youtube_subscribers": null, | |
| 9034 | + "facebook_followers": null, | |
| 9035 | + "x_followers": null, | |
| 9036 | + "twitch_followers": null | |
| 9037 | + }, | |
| 9038 | + "category": "autre", | |
| 9039 | + "location": "Québec", | |
| 9040 | + "notes": "Créateur de contenu sur l'islam (TikTok, Snapchat), partage sa vie de musulman au Japon. 5,3M abonnés cumulés selon Favikon (#4).", | |
| 9041 | + "sources": [ | |
| 9042 | + "favikon_qc" | |
| 9043 | + ], | |
| 9044 | + "total_followers": null, | |
| 9045 | + "platform_count": 0, | |
| 9046 | + "tier": "nano/inconnu" | |
| 9047 | + }, | |
| 9048 | + { | |
| 9049 | + "name": "Ceffi (CeffiCappi)", | |
| 9050 | + "handles": { | |
| 9051 | + "instagram": null, | |
| 9052 | + "tiktok": null, | |
| 9053 | + "youtube": null, | |
| 9054 | + "facebook": null, | |
| 9055 | + "x": null, | |
| 9056 | + "twitch": "cefficappi" | |
| 9057 | + }, | |
| 9058 | + "metrics": { | |
| 9059 | + "instagram_followers": null, | |
| 9060 | + "tiktok_followers": null, | |
| 9061 | + "youtube_subscribers": null, | |
| 9062 | + "facebook_followers": null, | |
| 9063 | + "x_followers": null, | |
| 9064 | + "twitch_followers": null | |
| 9065 | + }, | |
| 9066 | + "category": "gaming", | |
| 9067 | + "location": "Québec", | |
| 9068 | + "notes": "Créateur francophone multiplateforme, lives gaming et Just Chatting", | |
| 9069 | + "sources": [ | |
| 9070 | + "leregard_twitch" | |
| 9071 | + ], | |
| 9072 | + "total_followers": null, | |
| 9073 | + "platform_count": 1, | |
| 9074 | + "tier": "nano/inconnu" | |
| 9075 | + }, | |
| 9076 | + { | |
| 9077 | + "name": "On Déjeune", | |
| 9078 | + "handles": { | |
| 9079 | + "instagram": "ondejeune", | |
| 9080 | + "tiktok": null, | |
| 9081 | + "youtube": null, | |
| 9082 | + "facebook": null, | |
| 9083 | + "x": null, | |
| 9084 | + "twitch": null | |
| 9085 | + }, | |
| 9086 | + "metrics": { | |
| 9087 | + "instagram_followers": null, | |
| 9088 | + "tiktok_followers": null, | |
| 9089 | + "youtube_subscribers": null, | |
| 9090 | + "facebook_followers": null, | |
| 9091 | + "x_followers": null, | |
| 9092 | + "twitch_followers": null | |
| 9093 | + }, | |
| 9094 | + "category": "food", | |
| 9095 | + "location": "Montréal", | |
| 9096 | + "notes": "Compte de bonnes adresses à Montréal animé par @maybewats et @michelfleurs", | |
| 9097 | + "sources": [ | |
| 9098 | + "clark_food" | |
| 9099 | + ], | |
| 9100 | + "total_followers": null, | |
| 9101 | + "platform_count": 1, | |
| 9102 | + "tier": "nano/inconnu" | |
| 9103 | + }, | |
| 9104 | + { | |
| 9105 | + "name": "Ève Martel (Top Yummy)", | |
| 9106 | + "handles": { | |
| 9107 | + "instagram": "topyummy", | |
| 9108 | + "tiktok": null, | |
| 9109 | + "youtube": null, | |
| 9110 | + "facebook": null, | |
| 9111 | + "x": null, | |
| 9112 | + "twitch": null | |
| 9113 | + }, | |
| 9114 | + "metrics": { | |
| 9115 | + "instagram_followers": null, | |
| 9116 | + "tiktok_followers": null, | |
| 9117 | + "youtube_subscribers": null, | |
| 9118 | + "facebook_followers": null, | |
| 9119 | + "x_followers": null, | |
| 9120 | + "twitch_followers": null | |
| 9121 | + }, | |
| 9122 | + "category": "food", | |
| 9123 | + "location": "Québec", | |
| 9124 | + "notes": "Recettes santé, salades et meal preps sur Instagram et YouTube", | |
| 9125 | + "sources": [ | |
| 9126 | + "clark_food" | |
| 9127 | + ], | |
| 9128 | + "total_followers": null, | |
| 9129 | + "platform_count": 1, | |
| 9130 | + "tier": "nano/inconnu" | |
| 9131 | + }, | |
| 9132 | + { | |
| 9133 | + "name": "Christelle (Christelle is Flabbergasting)", | |
| 9134 | + "handles": { | |
| 9135 | + "instagram": "christellerocks", | |
| 9136 | + "tiktok": null, | |
| 9137 | + "youtube": null, | |
| 9138 | + "facebook": null, | |
| 9139 | + "x": null, | |
| 9140 | + "twitch": null | |
| 9141 | + }, | |
| 9142 | + "metrics": { | |
| 9143 | + "instagram_followers": null, | |
| 9144 | + "tiktok_followers": null, | |
| 9145 | + "youtube_subscribers": null, | |
| 9146 | + "facebook_followers": null, | |
| 9147 | + "x_followers": null, | |
| 9148 | + "twitch_followers": null | |
| 9149 | + }, | |
| 9150 | + "category": "food", | |
| 9151 | + "location": "Québec", | |
| 9152 | + "notes": "Photographe culinaire et auteure (Ensemble, cuisine gourmande et colorée)", | |
| 9153 | + "sources": [ | |
| 9154 | + "clark_food" | |
| 9155 | + ], | |
| 9156 | + "total_followers": null, | |
| 9157 | + "platform_count": 1, | |
| 9158 | + "tier": "nano/inconnu" | |
| 9159 | + }, | |
| 9160 | + { | |
| 9161 | + "name": "Kerrie Ahern", | |
| 9162 | + "handles": { | |
| 9163 | + "instagram": "kerrie_ahern", | |
| 9164 | + "tiktok": null, | |
| 9165 | + "youtube": null, | |
| 9166 | + "facebook": null, | |
| 9167 | + "x": null, | |
| 9168 | + "twitch": null | |
| 9169 | + }, | |
| 9170 | + "metrics": { | |
| 9171 | + "instagram_followers": null, | |
| 9172 | + "tiktok_followers": null, | |
| 9173 | + "youtube_subscribers": null, | |
| 9174 | + "facebook_followers": null, | |
| 9175 | + "x_followers": null, | |
| 9176 | + "twitch_followers": null | |
| 9177 | + }, | |
| 9178 | + "category": "food", | |
| 9179 | + "location": "Québec", | |
| 9180 | + "notes": "Recettes, mises en scène de repas et stylisme photo culinaire", | |
| 9181 | + "sources": [ | |
| 9182 | + "clark_food" | |
| 9183 | + ], | |
| 9184 | + "total_followers": null, | |
| 9185 | + "platform_count": 1, | |
| 9186 | + "tier": "nano/inconnu" | |
| 9187 | + }, | |
| 9188 | + { | |
| 9189 | + "name": "Chloé Crane-Leroux", | |
| 9190 | + "handles": { | |
| 9191 | + "instagram": "chloecleroux", | |
| 9192 | + "tiktok": null, | |
| 9193 | + "youtube": null, | |
| 9194 | + "facebook": null, | |
| 9195 | + "x": null, | |
| 9196 | + "twitch": null | |
| 9197 | + }, | |
| 9198 | + "metrics": { | |
| 9199 | + "instagram_followers": null, | |
| 9200 | + "tiktok_followers": null, | |
| 9201 | + "youtube_subscribers": null, | |
| 9202 | + "facebook_followers": null, | |
| 9203 | + "x_followers": null, | |
| 9204 | + "twitch_followers": null | |
| 9205 | + }, | |
| 9206 | + "category": "food", | |
| 9207 | + "location": "Montréal", | |
| 9208 | + "notes": "Photographe professionnelle entre New York et Montréal; co-auteure Les Saisons, La Table Vegane", | |
| 9209 | + "sources": [ | |
| 9210 | + "clark_food" | |
| 9211 | + ], | |
| 9212 | + "total_followers": null, | |
| 9213 | + "platform_count": 1, | |
| 9214 | + "tier": "nano/inconnu" | |
| 9215 | + }, | |
| 9216 | + { | |
| 9217 | + "name": "Gabrielle Pellerin", | |
| 9218 | + "handles": { | |
| 9219 | + "instagram": "gabepllrn", | |
| 9220 | + "tiktok": null, | |
| 9221 | + "youtube": null, | |
| 9222 | + "facebook": "chefgabriellepellerin", | |
| 9223 | + "x": null, | |
| 9224 | + "twitch": null | |
| 9225 | + }, | |
| 9226 | + "metrics": { | |
| 9227 | + "instagram_followers": null, | |
| 9228 | + "tiktok_followers": null, | |
| 9229 | + "youtube_subscribers": null, | |
| 9230 | + "facebook_followers": null, | |
| 9231 | + "x_followers": null, | |
| 9232 | + "twitch_followers": null | |
| 9233 | + }, | |
| 9234 | + "category": "food", | |
| 9235 | + "location": "Québec", | |
| 9236 | + "notes": "Chef cuisinière; recettes pour la box Cook It", | |
| 9237 | + "sources": [ | |
| 9238 | + "clark_food" | |
| 9239 | + ], | |
| 9240 | + "total_followers": null, | |
| 9241 | + "platform_count": 2, | |
| 9242 | + "tier": "nano/inconnu" | |
| 9243 | + }, | |
| 9244 | + { | |
| 9245 | + "name": "Sugar Sammy", | |
| 9246 | + "handles": { | |
| 9247 | + "instagram": null, | |
| 9248 | + "tiktok": null, | |
| 9249 | + "youtube": null, | |
| 9250 | + "facebook": null, | |
| 9251 | + "x": null, | |
| 9252 | + "twitch": null | |
| 9253 | + }, | |
| 9254 | + "metrics": { | |
| 9255 | + "instagram_followers": null, | |
| 9256 | + "tiktok_followers": null, | |
| 9257 | + "youtube_subscribers": null, | |
| 9258 | + "facebook_followers": null, | |
| 9259 | + "x_followers": null, | |
| 9260 | + "twitch_followers": null | |
| 9261 | + }, | |
| 9262 | + "category": "humour", | |
| 9263 | + "location": "Québec", | |
| 9264 | + "notes": "#2 humoriste QC: 1 790 900 abonnés cumulés (FB+IG+TikTok+X, 2024); retiré du top Twitter 2019 (ne vivait plus principalement au Québec)", | |
| 9265 | + "sources": [ | |
| 9266 | + "jdm_humoristes", | |
| 9267 | + "rythme_twitter" | |
| 9268 | + ], | |
| 9269 | + "total_followers": null, | |
| 9270 | + "platform_count": 0, | |
| 9271 | + "tier": "nano/inconnu" | |
| 9272 | + }, | |
| 9273 | + { | |
| 9274 | + "name": "Martin Matte", | |
| 9275 | + "handles": { | |
| 9276 | + "instagram": null, | |
| 9277 | + "tiktok": null, | |
| 9278 | + "youtube": null, | |
| 9279 | + "facebook": null, | |
| 9280 | + "x": null, | |
| 9281 | + "twitch": null | |
| 9282 | + }, | |
| 9283 | + "metrics": { | |
| 9284 | + "instagram_followers": null, | |
| 9285 | + "tiktok_followers": null, | |
| 9286 | + "youtube_subscribers": null, | |
| 9287 | + "facebook_followers": null, | |
| 9288 | + "x_followers": null, | |
| 9289 | + "twitch_followers": null | |
| 9290 | + }, | |
| 9291 | + "category": "humour", | |
| 9292 | + "location": "Québec", | |
| 9293 | + "notes": "#3 humoriste QC: 957 000 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9294 | + "sources": [ | |
| 9295 | + "jdm_humoristes" | |
| 9296 | + ], | |
| 9297 | + "total_followers": null, | |
| 9298 | + "platform_count": 0, | |
| 9299 | + "tier": "nano/inconnu" | |
| 9300 | + }, | |
| 9301 | + { | |
| 9302 | + "name": "Arnaud Soly", | |
| 9303 | + "handles": { | |
| 9304 | + "instagram": null, | |
| 9305 | + "tiktok": null, | |
| 9306 | + "youtube": null, | |
| 9307 | + "facebook": null, | |
| 9308 | + "x": null, | |
| 9309 | + "twitch": null | |
| 9310 | + }, | |
| 9311 | + "metrics": { | |
| 9312 | + "instagram_followers": null, | |
| 9313 | + "tiktok_followers": null, | |
| 9314 | + "youtube_subscribers": null, | |
| 9315 | + "facebook_followers": null, | |
| 9316 | + "x_followers": null, | |
| 9317 | + "twitch_followers": null | |
| 9318 | + }, | |
| 9319 | + "category": "humour", | |
| 9320 | + "location": "Québec", | |
| 9321 | + "notes": "#4 humoriste QC: 840 510 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9322 | + "sources": [ | |
| 9323 | + "jdm_humoristes" | |
| 9324 | + ], | |
| 9325 | + "total_followers": null, | |
| 9326 | + "platform_count": 0, | |
| 9327 | + "tier": "nano/inconnu" | |
| 9328 | + }, | |
| 9329 | + { | |
| 9330 | + "name": "Mike Ward", | |
| 9331 | + "handles": { | |
| 9332 | + "instagram": null, | |
| 9333 | + "tiktok": null, | |
| 9334 | + "youtube": null, | |
| 9335 | + "facebook": null, | |
| 9336 | + "x": "mikewardca", | |
| 9337 | + "twitch": null | |
| 9338 | + }, | |
| 9339 | + "metrics": { | |
| 9340 | + "instagram_followers": null, | |
| 9341 | + "tiktok_followers": null, | |
| 9342 | + "youtube_subscribers": null, | |
| 9343 | + "facebook_followers": null, | |
| 9344 | + "x_followers": null, | |
| 9345 | + "twitch_followers": null | |
| 9346 | + }, | |
| 9347 | + "category": "humour", | |
| 9348 | + "location": "Québec", | |
| 9349 | + "notes": "#5 humoriste QC: 820 394 abonnés cumulés (FB+IG+TikTok+X, 2024); parrain des podcasts d'humour; top 25 Twitter 2019 (>120K)", | |
| 9350 | + "sources": [ | |
| 9351 | + "jdm_humoristes", | |
| 9352 | + "rythme_twitter" | |
| 9353 | + ], | |
| 9354 | + "total_followers": null, | |
| 9355 | + "platform_count": 1, | |
| 9356 | + "tier": "nano/inconnu" | |
| 9357 | + }, | |
| 9358 | + { | |
| 9359 | + "name": "Mariana Mazza", | |
| 9360 | + "handles": { | |
| 9361 | + "instagram": null, | |
| 9362 | + "tiktok": null, | |
| 9363 | + "youtube": null, | |
| 9364 | + "facebook": null, | |
| 9365 | + "x": null, | |
| 9366 | + "twitch": null | |
| 9367 | + }, | |
| 9368 | + "metrics": { | |
| 9369 | + "instagram_followers": null, | |
| 9370 | + "tiktok_followers": null, | |
| 9371 | + "youtube_subscribers": null, | |
| 9372 | + "facebook_followers": null, | |
| 9373 | + "x_followers": null, | |
| 9374 | + "twitch_followers": null | |
| 9375 | + }, | |
| 9376 | + "category": "humour", | |
| 9377 | + "location": "Québec", | |
| 9378 | + "notes": "#6 humoriste QC: 738 700 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9379 | + "sources": [ | |
| 9380 | + "jdm_humoristes" | |
| 9381 | + ], | |
| 9382 | + "total_followers": null, | |
| 9383 | + "platform_count": 0, | |
| 9384 | + "tier": "nano/inconnu" | |
| 9385 | + }, | |
| 9386 | + { | |
| 9387 | + "name": "Anthony Kavanagh", | |
| 9388 | + "handles": { | |
| 9389 | + "instagram": null, | |
| 9390 | + "tiktok": null, | |
| 9391 | + "youtube": null, | |
| 9392 | + "facebook": null, | |
| 9393 | + "x": null, | |
| 9394 | + "twitch": null | |
| 9395 | + }, | |
| 9396 | + "metrics": { | |
| 9397 | + "instagram_followers": null, | |
| 9398 | + "tiktok_followers": null, | |
| 9399 | + "youtube_subscribers": null, | |
| 9400 | + "facebook_followers": null, | |
| 9401 | + "x_followers": null, | |
| 9402 | + "twitch_followers": null | |
| 9403 | + }, | |
| 9404 | + "category": "humour", | |
| 9405 | + "location": "Québec", | |
| 9406 | + "notes": "#7 humoriste QC: 713 832 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9407 | + "sources": [ | |
| 9408 | + "jdm_humoristes" | |
| 9409 | + ], | |
| 9410 | + "total_followers": null, | |
| 9411 | + "platform_count": 0, | |
| 9412 | + "tier": "nano/inconnu" | |
| 9413 | + }, | |
| 9414 | + { | |
| 9415 | + "name": "Jérémy Demay", | |
| 9416 | + "handles": { | |
| 9417 | + "instagram": null, | |
| 9418 | + "tiktok": null, | |
| 9419 | + "youtube": null, | |
| 9420 | + "facebook": null, | |
| 9421 | + "x": null, | |
| 9422 | + "twitch": null | |
| 9423 | + }, | |
| 9424 | + "metrics": { | |
| 9425 | + "instagram_followers": null, | |
| 9426 | + "tiktok_followers": null, | |
| 9427 | + "youtube_subscribers": null, | |
| 9428 | + "facebook_followers": null, | |
| 9429 | + "x_followers": null, | |
| 9430 | + "twitch_followers": null | |
| 9431 | + }, | |
| 9432 | + "category": "humour", | |
| 9433 | + "location": "Québec", | |
| 9434 | + "notes": "#8 humoriste QC: 690 217 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9435 | + "sources": [ | |
| 9436 | + "jdm_humoristes" | |
| 9437 | + ], | |
| 9438 | + "total_followers": null, | |
| 9439 | + "platform_count": 0, | |
| 9440 | + "tier": "nano/inconnu" | |
| 9441 | + }, | |
| 9442 | + { | |
| 9443 | + "name": "Jay Du Temple", | |
| 9444 | + "handles": { | |
| 9445 | + "instagram": null, | |
| 9446 | + "tiktok": null, | |
| 9447 | + "youtube": null, | |
| 9448 | + "facebook": null, | |
| 9449 | + "x": null, | |
| 9450 | + "twitch": null | |
| 9451 | + }, | |
| 9452 | + "metrics": { | |
| 9453 | + "instagram_followers": null, | |
| 9454 | + "tiktok_followers": null, | |
| 9455 | + "youtube_subscribers": null, | |
| 9456 | + "facebook_followers": null, | |
| 9457 | + "x_followers": null, | |
| 9458 | + "twitch_followers": null | |
| 9459 | + }, | |
| 9460 | + "category": "humour", | |
| 9461 | + "location": "Québec", | |
| 9462 | + "notes": "#10 humoriste QC: 632 203 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9463 | + "sources": [ | |
| 9464 | + "jdm_humoristes" | |
| 9465 | + ], | |
| 9466 | + "total_followers": null, | |
| 9467 | + "platform_count": 0, | |
| 9468 | + "tier": "nano/inconnu" | |
| 9469 | + }, | |
| 9470 | + { | |
| 9471 | + "name": "Louis-José Houde", | |
| 9472 | + "handles": { | |
| 9473 | + "instagram": null, | |
| 9474 | + "tiktok": null, | |
| 9475 | + "youtube": null, | |
| 9476 | + "facebook": null, | |
| 9477 | + "x": null, | |
| 9478 | + "twitch": null | |
| 9479 | + }, | |
| 9480 | + "metrics": { | |
| 9481 | + "instagram_followers": null, | |
| 9482 | + "tiktok_followers": null, | |
| 9483 | + "youtube_subscribers": null, | |
| 9484 | + "facebook_followers": null, | |
| 9485 | + "x_followers": null, | |
| 9486 | + "twitch_followers": null | |
| 9487 | + }, | |
| 9488 | + "category": "humour", | |
| 9489 | + "location": "Québec", | |
| 9490 | + "notes": "#11 humoriste QC: 581 400 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9491 | + "sources": [ | |
| 9492 | + "jdm_humoristes" | |
| 9493 | + ], | |
| 9494 | + "total_followers": null, | |
| 9495 | + "platform_count": 0, | |
| 9496 | + "tier": "nano/inconnu" | |
| 9497 | + }, | |
| 9498 | + { | |
| 9499 | + "name": "François Bellefeuille", | |
| 9500 | + "handles": { | |
| 9501 | + "instagram": null, | |
| 9502 | + "tiktok": null, | |
| 9503 | + "youtube": null, | |
| 9504 | + "facebook": null, | |
| 9505 | + "x": null, | |
| 9506 | + "twitch": null | |
| 9507 | + }, | |
| 9508 | + "metrics": { | |
| 9509 | + "instagram_followers": null, | |
| 9510 | + "tiktok_followers": null, | |
| 9511 | + "youtube_subscribers": null, | |
| 9512 | + "facebook_followers": null, | |
| 9513 | + "x_followers": null, | |
| 9514 | + "twitch_followers": null | |
| 9515 | + }, | |
| 9516 | + "category": "humour", | |
| 9517 | + "location": "Québec", | |
| 9518 | + "notes": "#13 humoriste QC: 526 900 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9519 | + "sources": [ | |
| 9520 | + "jdm_humoristes" | |
| 9521 | + ], | |
| 9522 | + "total_followers": null, | |
| 9523 | + "platform_count": 0, | |
| 9524 | + "tier": "nano/inconnu" | |
| 9525 | + }, | |
| 9526 | + { | |
| 9527 | + "name": "Philippe Laprise", | |
| 9528 | + "handles": { | |
| 9529 | + "instagram": null, | |
| 9530 | + "tiktok": null, | |
| 9531 | + "youtube": null, | |
| 9532 | + "facebook": null, | |
| 9533 | + "x": null, | |
| 9534 | + "twitch": null | |
| 9535 | + }, | |
| 9536 | + "metrics": { | |
| 9537 | + "instagram_followers": null, | |
| 9538 | + "tiktok_followers": null, | |
| 9539 | + "youtube_subscribers": null, | |
| 9540 | + "facebook_followers": null, | |
| 9541 | + "x_followers": null, | |
| 9542 | + "twitch_followers": null | |
| 9543 | + }, | |
| 9544 | + "category": "humour", | |
| 9545 | + "location": "Québec", | |
| 9546 | + "notes": "#14 humoriste QC: 517 100 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9547 | + "sources": [ | |
| 9548 | + "jdm_humoristes" | |
| 9549 | + ], | |
| 9550 | + "total_followers": null, | |
| 9551 | + "platform_count": 0, | |
| 9552 | + "tier": "nano/inconnu" | |
| 9553 | + }, | |
| 9554 | + { | |
| 9555 | + "name": "Pierre-Yves Roy-Desmarais", | |
| 9556 | + "handles": { | |
| 9557 | + "instagram": null, | |
| 9558 | + "tiktok": null, | |
| 9559 | + "youtube": null, | |
| 9560 | + "facebook": null, | |
| 9561 | + "x": null, | |
| 9562 | + "twitch": null | |
| 9563 | + }, | |
| 9564 | + "metrics": { | |
| 9565 | + "instagram_followers": null, | |
| 9566 | + "tiktok_followers": null, | |
| 9567 | + "youtube_subscribers": null, | |
| 9568 | + "facebook_followers": null, | |
| 9569 | + "x_followers": null, | |
| 9570 | + "twitch_followers": null | |
| 9571 | + }, | |
| 9572 | + "category": "humour", | |
| 9573 | + "location": "Québec", | |
| 9574 | + "notes": "#15 humoriste QC: 480 700 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9575 | + "sources": [ | |
| 9576 | + "jdm_humoristes" | |
| 9577 | + ], | |
| 9578 | + "total_followers": null, | |
| 9579 | + "platform_count": 0, | |
| 9580 | + "tier": "nano/inconnu" | |
| 9581 | + }, | |
| 9582 | + { | |
| 9583 | + "name": "Katherine Levac", | |
| 9584 | + "handles": { | |
| 9585 | + "instagram": null, | |
| 9586 | + "tiktok": null, | |
| 9587 | + "youtube": null, | |
| 9588 | + "facebook": null, | |
| 9589 | + "x": null, | |
| 9590 | + "twitch": null | |
| 9591 | + }, | |
| 9592 | + "metrics": { | |
| 9593 | + "instagram_followers": null, | |
| 9594 | + "tiktok_followers": null, | |
| 9595 | + "youtube_subscribers": null, | |
| 9596 | + "facebook_followers": null, | |
| 9597 | + "x_followers": null, | |
| 9598 | + "twitch_followers": null | |
| 9599 | + }, | |
| 9600 | + "category": "humour", | |
| 9601 | + "location": "Québec", | |
| 9602 | + "notes": "#16 humoriste QC: 479 060 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9603 | + "sources": [ | |
| 9604 | + "jdm_humoristes" | |
| 9605 | + ], | |
| 9606 | + "total_followers": null, | |
| 9607 | + "platform_count": 0, | |
| 9608 | + "tier": "nano/inconnu" | |
| 9609 | + }, | |
| 9610 | + { | |
| 9611 | + "name": "Louis Morissette", | |
| 9612 | + "handles": { | |
| 9613 | + "instagram": null, | |
| 9614 | + "tiktok": null, | |
| 9615 | + "youtube": null, | |
| 9616 | + "facebook": null, | |
| 9617 | + "x": null, | |
| 9618 | + "twitch": null | |
| 9619 | + }, | |
| 9620 | + "metrics": { | |
| 9621 | + "instagram_followers": null, | |
| 9622 | + "tiktok_followers": null, | |
| 9623 | + "youtube_subscribers": null, | |
| 9624 | + "facebook_followers": null, | |
| 9625 | + "x_followers": null, | |
| 9626 | + "twitch_followers": null | |
| 9627 | + }, | |
| 9628 | + "category": "humour", | |
| 9629 | + "location": "Québec", | |
| 9630 | + "notes": "#17 humoriste QC: 475 800 abonnés cumulés (FB+IG+TikTok+X, 2024); aussi au top 25 Twitter 2019 (>120K)", | |
| 9631 | + "sources": [ | |
| 9632 | + "jdm_humoristes", | |
| 9633 | + "rythme_twitter" | |
| 9634 | + ], | |
| 9635 | + "total_followers": null, | |
| 9636 | + "platform_count": 0, | |
| 9637 | + "tier": "nano/inconnu" | |
| 9638 | + }, | |
| 9639 | + { | |
| 9640 | + "name": "Liliane Blanco-Binette", | |
| 9641 | + "handles": { | |
| 9642 | + "instagram": null, | |
| 9643 | + "tiktok": null, | |
| 9644 | + "youtube": null, | |
| 9645 | + "facebook": null, | |
| 9646 | + "x": null, | |
| 9647 | + "twitch": null | |
| 9648 | + }, | |
| 9649 | + "metrics": { | |
| 9650 | + "instagram_followers": null, | |
| 9651 | + "tiktok_followers": null, | |
| 9652 | + "youtube_subscribers": null, | |
| 9653 | + "facebook_followers": null, | |
| 9654 | + "x_followers": null, | |
| 9655 | + "twitch_followers": null | |
| 9656 | + }, | |
| 9657 | + "category": "humour", | |
| 9658 | + "location": "Québec", | |
| 9659 | + "notes": "#18 humoriste QC: 471 800 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9660 | + "sources": [ | |
| 9661 | + "jdm_humoristes" | |
| 9662 | + ], | |
| 9663 | + "total_followers": null, | |
| 9664 | + "platform_count": 0, | |
| 9665 | + "tier": "nano/inconnu" | |
| 9666 | + }, | |
| 9667 | + { | |
| 9668 | + "name": "Stéphane Rousseau", | |
| 9669 | + "handles": { | |
| 9670 | + "instagram": null, | |
| 9671 | + "tiktok": null, | |
| 9672 | + "youtube": null, | |
| 9673 | + "facebook": null, | |
| 9674 | + "x": null, | |
| 9675 | + "twitch": null | |
| 9676 | + }, | |
| 9677 | + "metrics": { | |
| 9678 | + "instagram_followers": null, | |
| 9679 | + "tiktok_followers": null, | |
| 9680 | + "youtube_subscribers": null, | |
| 9681 | + "facebook_followers": null, | |
| 9682 | + "x_followers": null, | |
| 9683 | + "twitch_followers": null | |
| 9684 | + }, | |
| 9685 | + "category": "humour", | |
| 9686 | + "location": "Québec", | |
| 9687 | + "notes": "#19 humoriste QC: 465 100 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9688 | + "sources": [ | |
| 9689 | + "jdm_humoristes" | |
| 9690 | + ], | |
| 9691 | + "total_followers": null, | |
| 9692 | + "platform_count": 0, | |
| 9693 | + "tier": "nano/inconnu" | |
| 9694 | + }, | |
| 9695 | + { | |
| 9696 | + "name": "Mathieu Cyr", | |
| 9697 | + "handles": { | |
| 9698 | + "instagram": null, | |
| 9699 | + "tiktok": null, | |
| 9700 | + "youtube": null, | |
| 9701 | + "facebook": null, | |
| 9702 | + "x": null, | |
| 9703 | + "twitch": null | |
| 9704 | + }, | |
| 9705 | + "metrics": { | |
| 9706 | + "instagram_followers": null, | |
| 9707 | + "tiktok_followers": null, | |
| 9708 | + "youtube_subscribers": null, | |
| 9709 | + "facebook_followers": null, | |
| 9710 | + "x_followers": null, | |
| 9711 | + "twitch_followers": null | |
| 9712 | + }, | |
| 9713 | + "category": "humour", | |
| 9714 | + "location": "Québec", | |
| 9715 | + "notes": "#20 humoriste QC: 446 800 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9716 | + "sources": [ | |
| 9717 | + "jdm_humoristes" | |
| 9718 | + ], | |
| 9719 | + "total_followers": null, | |
| 9720 | + "platform_count": 0, | |
| 9721 | + "tier": "nano/inconnu" | |
| 9722 | + }, | |
| 9723 | + { | |
| 9724 | + "name": "Pierre Hébert", | |
| 9725 | + "handles": { | |
| 9726 | + "instagram": null, | |
| 9727 | + "tiktok": null, | |
| 9728 | + "youtube": null, | |
| 9729 | + "facebook": null, | |
| 9730 | + "x": null, | |
| 9731 | + "twitch": null | |
| 9732 | + }, | |
| 9733 | + "metrics": { | |
| 9734 | + "instagram_followers": null, | |
| 9735 | + "tiktok_followers": null, | |
| 9736 | + "youtube_subscribers": null, | |
| 9737 | + "facebook_followers": null, | |
| 9738 | + "x_followers": null, | |
| 9739 | + "twitch_followers": null | |
| 9740 | + }, | |
| 9741 | + "category": "humour", | |
| 9742 | + "location": "Québec", | |
| 9743 | + "notes": "#21 humoriste QC: 439 200 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9744 | + "sources": [ | |
| 9745 | + "jdm_humoristes" | |
| 9746 | + ], | |
| 9747 | + "total_followers": null, | |
| 9748 | + "platform_count": 0, | |
| 9749 | + "tier": "nano/inconnu" | |
| 9750 | + }, | |
| 9751 | + { | |
| 9752 | + "name": "Marie-Lyne Joncas", | |
| 9753 | + "handles": { | |
| 9754 | + "instagram": null, | |
| 9755 | + "tiktok": null, | |
| 9756 | + "youtube": null, | |
| 9757 | + "facebook": null, | |
| 9758 | + "x": null, | |
| 9759 | + "twitch": null | |
| 9760 | + }, | |
| 9761 | + "metrics": { | |
| 9762 | + "instagram_followers": null, | |
| 9763 | + "tiktok_followers": null, | |
| 9764 | + "youtube_subscribers": null, | |
| 9765 | + "facebook_followers": null, | |
| 9766 | + "x_followers": null, | |
| 9767 | + "twitch_followers": null | |
| 9768 | + }, | |
| 9769 | + "category": "humour", | |
| 9770 | + "location": "Québec", | |
| 9771 | + "notes": "#22 humoriste QC: 414 185 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9772 | + "sources": [ | |
| 9773 | + "jdm_humoristes" | |
| 9774 | + ], | |
| 9775 | + "total_followers": null, | |
| 9776 | + "platform_count": 0, | |
| 9777 | + "tier": "nano/inconnu" | |
| 9778 | + }, | |
| 9779 | + { | |
| 9780 | + "name": "Alex Perron", | |
| 9781 | + "handles": { | |
| 9782 | + "instagram": null, | |
| 9783 | + "tiktok": null, | |
| 9784 | + "youtube": null, | |
| 9785 | + "facebook": null, | |
| 9786 | + "x": "alexxxperron", | |
| 9787 | + "twitch": null | |
| 9788 | + }, | |
| 9789 | + "metrics": { | |
| 9790 | + "instagram_followers": null, | |
| 9791 | + "tiktok_followers": null, | |
| 9792 | + "youtube_subscribers": null, | |
| 9793 | + "facebook_followers": null, | |
| 9794 | + "x_followers": null, | |
| 9795 | + "twitch_followers": null | |
| 9796 | + }, | |
| 9797 | + "category": "humour", | |
| 9798 | + "location": "Québec", | |
| 9799 | + "notes": "#23 humoriste QC: 382 700 abonnés cumulés (FB+IG+TikTok+X, 2024); aussi au top 25 Twitter 2019 (>120K)", | |
| 9800 | + "sources": [ | |
| 9801 | + "jdm_humoristes", | |
| 9802 | + "rythme_twitter" | |
| 9803 | + ], | |
| 9804 | + "total_followers": null, | |
| 9805 | + "platform_count": 1, | |
| 9806 | + "tier": "nano/inconnu" | |
| 9807 | + }, | |
| 9808 | + { | |
| 9809 | + "name": "Jean-Marc Parent", | |
| 9810 | + "handles": { | |
| 9811 | + "instagram": null, | |
| 9812 | + "tiktok": null, | |
| 9813 | + "youtube": null, | |
| 9814 | + "facebook": null, | |
| 9815 | + "x": null, | |
| 9816 | + "twitch": null | |
| 9817 | + }, | |
| 9818 | + "metrics": { | |
| 9819 | + "instagram_followers": null, | |
| 9820 | + "tiktok_followers": null, | |
| 9821 | + "youtube_subscribers": null, | |
| 9822 | + "facebook_followers": null, | |
| 9823 | + "x_followers": null, | |
| 9824 | + "twitch_followers": null | |
| 9825 | + }, | |
| 9826 | + "category": "humour", | |
| 9827 | + "location": "Québec", | |
| 9828 | + "notes": "#24 humoriste QC: 376 300 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9829 | + "sources": [ | |
| 9830 | + "jdm_humoristes" | |
| 9831 | + ], | |
| 9832 | + "total_followers": null, | |
| 9833 | + "platform_count": 0, | |
| 9834 | + "tier": "nano/inconnu" | |
| 9835 | + }, | |
| 9836 | + { | |
| 9837 | + "name": "Jean-François Mercier", | |
| 9838 | + "handles": { | |
| 9839 | + "instagram": null, | |
| 9840 | + "tiktok": null, | |
| 9841 | + "youtube": null, | |
| 9842 | + "facebook": null, | |
| 9843 | + "x": null, | |
| 9844 | + "twitch": null | |
| 9845 | + }, | |
| 9846 | + "metrics": { | |
| 9847 | + "instagram_followers": null, | |
| 9848 | + "tiktok_followers": null, | |
| 9849 | + "youtube_subscribers": null, | |
| 9850 | + "facebook_followers": null, | |
| 9851 | + "x_followers": null, | |
| 9852 | + "twitch_followers": null | |
| 9853 | + }, | |
| 9854 | + "category": "humour", | |
| 9855 | + "location": "Québec", | |
| 9856 | + "notes": "#25 humoriste QC: 373 900 abonnés cumulés (FB+IG+TikTok+X, 2024)", | |
| 9857 | + "sources": [ | |
| 9858 | + "jdm_humoristes" | |
| 9859 | + ], | |
| 9860 | + "total_followers": null, | |
| 9861 | + "platform_count": 0, | |
| 9862 | + "tier": "nano/inconnu" | |
| 9863 | + }, | |
| 9864 | + { | |
| 9865 | + "name": "Tommy Néron", | |
| 9866 | + "handles": { | |
| 9867 | + "instagram": null, | |
| 9868 | + "tiktok": null, | |
| 9869 | + "youtube": null, | |
| 9870 | + "facebook": null, | |
| 9871 | + "x": null, | |
| 9872 | + "twitch": null | |
| 9873 | + }, | |
| 9874 | + "metrics": { | |
| 9875 | + "instagram_followers": null, | |
| 9876 | + "tiktok_followers": null, | |
| 9877 | + "youtube_subscribers": null, | |
| 9878 | + "facebook_followers": null, | |
| 9879 | + "x_followers": null, | |
| 9880 | + "twitch_followers": null | |
| 9881 | + }, | |
| 9882 | + "category": "humour", | |
| 9883 | + "location": "Québec", | |
| 9884 | + "notes": "Humoriste de la relève cité dans l'article JdM; auditions obtenues via TikTok/Instagram", | |
| 9885 | + "sources": [ | |
| 9886 | + "jdm_humoristes" | |
| 9887 | + ], | |
| 9888 | + "total_followers": null, | |
| 9889 | + "platform_count": 0, | |
| 9890 | + "tier": "nano/inconnu" | |
| 9891 | + }, | |
| 9892 | + { | |
| 9893 | + "name": "Charles Pellerin", | |
| 9894 | + "handles": { | |
| 9895 | + "instagram": null, | |
| 9896 | + "tiktok": null, | |
| 9897 | + "youtube": null, | |
| 9898 | + "facebook": null, | |
| 9899 | + "x": null, | |
| 9900 | + "twitch": null | |
| 9901 | + }, | |
| 9902 | + "metrics": { | |
| 9903 | + "instagram_followers": null, | |
| 9904 | + "tiktok_followers": null, | |
| 9905 | + "youtube_subscribers": null, | |
| 9906 | + "facebook_followers": null, | |
| 9907 | + "x_followers": null, | |
| 9908 | + "twitch_followers": null | |
| 9909 | + }, | |
| 9910 | + "category": "humour", | |
| 9911 | + "location": "Québec", | |
| 9912 | + "notes": "Humoriste cité dans l'article JdM sur l'importance des réseaux sociaux", | |
| 9913 | + "sources": [ | |
| 9914 | + "jdm_humoristes" | |
| 9915 | + ], | |
| 9916 | + "total_followers": null, | |
| 9917 | + "platform_count": 0, | |
| 9918 | + "tier": "nano/inconnu" | |
| 9919 | + }, | |
| 9920 | + { | |
| 9921 | + "name": "Jean-Michel Martel", | |
| 9922 | + "handles": { | |
| 9923 | + "instagram": null, | |
| 9924 | + "tiktok": null, | |
| 9925 | + "youtube": null, | |
| 9926 | + "facebook": null, | |
| 9927 | + "x": null, | |
| 9928 | + "twitch": null | |
| 9929 | + }, | |
| 9930 | + "metrics": { | |
| 9931 | + "instagram_followers": null, | |
| 9932 | + "tiktok_followers": null, | |
| 9933 | + "youtube_subscribers": null, | |
| 9934 | + "facebook_followers": null, | |
| 9935 | + "x_followers": null, | |
| 9936 | + "twitch_followers": null | |
| 9937 | + }, | |
| 9938 | + "category": "humour", | |
| 9939 | + "location": "Québec", | |
| 9940 | + "notes": "Humoriste cité dans l'article JdM sur l'importance des réseaux sociaux", | |
| 9941 | + "sources": [ | |
| 9942 | + "jdm_humoristes" | |
| 9943 | + ], | |
| 9944 | + "total_followers": null, | |
| 9945 | + "platform_count": 0, | |
| 9946 | + "tier": "nano/inconnu" | |
| 9947 | + }, | |
| 9948 | + { | |
| 9949 | + "name": "Guy A. Lepage", | |
| 9950 | + "handles": { | |
| 9951 | + "instagram": null, | |
| 9952 | + "tiktok": null, | |
| 9953 | + "youtube": null, | |
| 9954 | + "facebook": null, | |
| 9955 | + "x": "guyalepage", | |
| 9956 | + "twitch": null | |
| 9957 | + }, | |
| 9958 | + "metrics": { | |
| 9959 | + "instagram_followers": null, | |
| 9960 | + "tiktok_followers": null, | |
| 9961 | + "youtube_subscribers": null, | |
| 9962 | + "facebook_followers": null, | |
| 9963 | + "x_followers": null, | |
| 9964 | + "twitch_followers": null | |
| 9965 | + }, | |
| 9966 | + "category": "médias", | |
| 9967 | + "location": "Québec", | |
| 9968 | + "notes": "#1 du top 25 personnalités QC les plus suivies sur Twitter (2019, >120K abonnés); animateur de Tout le monde en parle", | |
| 9969 | + "sources": [ | |
| 9970 | + "rythme_twitter" | |
| 9971 | + ], | |
| 9972 | + "total_followers": null, | |
| 9973 | + "platform_count": 1, | |
| 9974 | + "tier": "nano/inconnu" | |
| 9975 | + }, | |
| 9976 | + { | |
| 9977 | + "name": "Dominic Arpin", | |
| 9978 | + "handles": { | |
| 9979 | + "instagram": null, | |
| 9980 | + "tiktok": null, | |
| 9981 | + "youtube": null, | |
| 9982 | + "facebook": null, | |
| 9983 | + "x": null, | |
| 9984 | + "twitch": null | |
| 9985 | + }, | |
| 9986 | + "metrics": { | |
| 9987 | + "instagram_followers": null, | |
| 9988 | + "tiktok_followers": null, | |
| 9989 | + "youtube_subscribers": null, | |
| 9990 | + "facebook_followers": null, | |
| 9991 | + "x_followers": null, | |
| 9992 | + "twitch_followers": null | |
| 9993 | + }, | |
| 9994 | + "category": "médias", | |
| 9995 | + "location": "Québec", | |
| 9996 | + "notes": "#2 du top 25 Twitter QC 2019 (>120K abonnés); animateur/chroniqueur", | |
| 9997 | + "sources": [ | |
| 9998 | + "rythme_twitter" | |
| 9999 | + ], | |
| 10000 | + "total_followers": null, | |
| 10001 | + "platform_count": 0, | |
| 10002 | + "tier": "nano/inconnu" | |
| 10003 | + }, | |
| 10004 | + { | |
| 10005 | + "name": "Véronique Cloutier", | |
| 10006 | + "handles": { | |
| 10007 | + "instagram": null, | |
| 10008 | + "tiktok": null, | |
| 10009 | + "youtube": null, | |
| 10010 | + "facebook": null, | |
| 10011 | + "x": null, | |
| 10012 | + "twitch": null | |
| 10013 | + }, | |
| 10014 | + "metrics": { | |
| 10015 | + "instagram_followers": null, | |
| 10016 | + "tiktok_followers": null, | |
| 10017 | + "youtube_subscribers": null, | |
| 10018 | + "facebook_followers": null, | |
| 10019 | + "x_followers": null, | |
| 10020 | + "twitch_followers": null | |
| 10021 | + }, | |
| 10022 | + "category": "médias", | |
| 10023 | + "location": "Québec", | |
| 10024 | + "notes": "#3 du top 25 Twitter QC 2019 (>120K abonnés); animatrice", | |
| 10025 | + "sources": [ | |
| 10026 | + "rythme_twitter" | |
| 10027 | + ], | |
| 10028 | + "total_followers": null, | |
| 10029 | + "platform_count": 0, | |
| 10030 | + "tier": "nano/inconnu" | |
| 10031 | + }, | |
| 10032 | + { | |
| 10033 | + "name": "Cœur de Pirate (Béatrice Martin)", | |
| 10034 | + "handles": { | |
| 10035 | + "instagram": null, | |
| 10036 | + "tiktok": null, | |
| 10037 | + "youtube": null, | |
| 10038 | + "facebook": null, | |
| 10039 | + "x": null, | |
| 10040 | + "twitch": null | |
| 10041 | + }, | |
| 10042 | + "metrics": { | |
| 10043 | + "instagram_followers": null, | |
| 10044 | + "tiktok_followers": null, | |
| 10045 | + "youtube_subscribers": null, | |
| 10046 | + "facebook_followers": null, | |
| 10047 | + "x_followers": null, | |
| 10048 | + "twitch_followers": null | |
| 10049 | + }, | |
| 10050 | + "category": "musique", | |
| 10051 | + "location": "Québec", | |
| 10052 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); +20,8% d'abonnés cette année-là", | |
| 10053 | + "sources": [ | |
| 10054 | + "rythme_twitter" | |
| 10055 | + ], | |
| 10056 | + "total_followers": null, | |
| 10057 | + "platform_count": 0, | |
| 10058 | + "tier": "nano/inconnu" | |
| 10059 | + }, | |
| 10060 | + { | |
| 10061 | + "name": "Richard Martineau", | |
| 10062 | + "handles": { | |
| 10063 | + "instagram": null, | |
| 10064 | + "tiktok": null, | |
| 10065 | + "youtube": null, | |
| 10066 | + "facebook": null, | |
| 10067 | + "x": null, | |
| 10068 | + "twitch": null | |
| 10069 | + }, | |
| 10070 | + "metrics": { | |
| 10071 | + "instagram_followers": null, | |
| 10072 | + "tiktok_followers": null, | |
| 10073 | + "youtube_subscribers": null, | |
| 10074 | + "facebook_followers": null, | |
| 10075 | + "x_followers": null, | |
| 10076 | + "twitch_followers": null | |
| 10077 | + }, | |
| 10078 | + "category": "médias", | |
| 10079 | + "location": "Québec", | |
| 10080 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); +2,5% d'abonnés; chroniqueur", | |
| 10081 | + "sources": [ | |
| 10082 | + "rythme_twitter" | |
| 10083 | + ], | |
| 10084 | + "total_followers": null, | |
| 10085 | + "platform_count": 0, | |
| 10086 | + "tier": "nano/inconnu" | |
| 10087 | + }, | |
| 10088 | + { | |
| 10089 | + "name": "Denis Coderre", | |
| 10090 | + "handles": { | |
| 10091 | + "instagram": null, | |
| 10092 | + "tiktok": null, | |
| 10093 | + "youtube": null, | |
| 10094 | + "facebook": null, | |
| 10095 | + "x": null, | |
| 10096 | + "twitch": null | |
| 10097 | + }, | |
| 10098 | + "metrics": { | |
| 10099 | + "instagram_followers": null, | |
| 10100 | + "tiktok_followers": null, | |
| 10101 | + "youtube_subscribers": null, | |
| 10102 | + "facebook_followers": null, | |
| 10103 | + "x_followers": null, | |
| 10104 | + "twitch_followers": null | |
| 10105 | + }, | |
| 10106 | + "category": "autre", | |
| 10107 | + "location": "Montréal", | |
| 10108 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); politicien, ex-maire de Montréal", | |
| 10109 | + "sources": [ | |
| 10110 | + "rythme_twitter" | |
| 10111 | + ], | |
| 10112 | + "total_followers": null, | |
| 10113 | + "platform_count": 0, | |
| 10114 | + "tier": "nano/inconnu" | |
| 10115 | + }, | |
| 10116 | + { | |
| 10117 | + "name": "Patrick Lagacé", | |
| 10118 | + "handles": { | |
| 10119 | + "instagram": null, | |
| 10120 | + "tiktok": null, | |
| 10121 | + "youtube": null, | |
| 10122 | + "facebook": null, | |
| 10123 | + "x": null, | |
| 10124 | + "twitch": null | |
| 10125 | + }, | |
| 10126 | + "metrics": { | |
| 10127 | + "instagram_followers": null, | |
| 10128 | + "tiktok_followers": null, | |
| 10129 | + "youtube_subscribers": null, | |
| 10130 | + "facebook_followers": null, | |
| 10131 | + "x_followers": null, | |
| 10132 | + "twitch_followers": null | |
| 10133 | + }, | |
| 10134 | + "category": "médias", | |
| 10135 | + "location": "Québec", | |
| 10136 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); journaliste-chroniqueur", | |
| 10137 | + "sources": [ | |
| 10138 | + "rythme_twitter" | |
| 10139 | + ], | |
| 10140 | + "total_followers": null, | |
| 10141 | + "platform_count": 0, | |
| 10142 | + "tier": "nano/inconnu" | |
| 10143 | + }, | |
| 10144 | + { | |
| 10145 | + "name": "Georges Laraque", | |
| 10146 | + "handles": { | |
| 10147 | + "instagram": null, | |
| 10148 | + "tiktok": null, | |
| 10149 | + "youtube": null, | |
| 10150 | + "facebook": null, | |
| 10151 | + "x": null, | |
| 10152 | + "twitch": null | |
| 10153 | + }, | |
| 10154 | + "metrics": { | |
| 10155 | + "instagram_followers": null, | |
| 10156 | + "tiktok_followers": null, | |
| 10157 | + "youtube_subscribers": null, | |
| 10158 | + "facebook_followers": null, | |
| 10159 | + "x_followers": null, | |
| 10160 | + "twitch_followers": null | |
| 10161 | + }, | |
| 10162 | + "category": "autre", | |
| 10163 | + "location": "Québec", | |
| 10164 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); ex-joueur de la LNH", | |
| 10165 | + "sources": [ | |
| 10166 | + "rythme_twitter" | |
| 10167 | + ], | |
| 10168 | + "total_followers": null, | |
| 10169 | + "platform_count": 0, | |
| 10170 | + "tier": "nano/inconnu" | |
| 10171 | + }, | |
| 10172 | + { | |
| 10173 | + "name": "Dany Turcotte", | |
| 10174 | + "handles": { | |
| 10175 | + "instagram": null, | |
| 10176 | + "tiktok": null, | |
| 10177 | + "youtube": null, | |
| 10178 | + "facebook": null, | |
| 10179 | + "x": "danyturcotte", | |
| 10180 | + "twitch": null | |
| 10181 | + }, | |
| 10182 | + "metrics": { | |
| 10183 | + "instagram_followers": null, | |
| 10184 | + "tiktok_followers": null, | |
| 10185 | + "youtube_subscribers": null, | |
| 10186 | + "facebook_followers": null, | |
| 10187 | + "x_followers": null, | |
| 10188 | + "twitch_followers": null | |
| 10189 | + }, | |
| 10190 | + "category": "humour", | |
| 10191 | + "location": "Québec", | |
| 10192 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); humoriste, ex-fou du roi de TLMEP", | |
| 10193 | + "sources": [ | |
| 10194 | + "rythme_twitter" | |
| 10195 | + ], | |
| 10196 | + "total_followers": null, | |
| 10197 | + "platform_count": 1, | |
| 10198 | + "tier": "nano/inconnu" | |
| 10199 | + }, | |
| 10200 | + { | |
| 10201 | + "name": "Julie Snyder", | |
| 10202 | + "handles": { | |
| 10203 | + "instagram": null, | |
| 10204 | + "tiktok": null, | |
| 10205 | + "youtube": null, | |
| 10206 | + "facebook": null, | |
| 10207 | + "x": "snyderjulie", | |
| 10208 | + "twitch": null | |
| 10209 | + }, | |
| 10210 | + "metrics": { | |
| 10211 | + "instagram_followers": null, | |
| 10212 | + "tiktok_followers": null, | |
| 10213 | + "youtube_subscribers": null, | |
| 10214 | + "facebook_followers": null, | |
| 10215 | + "x_followers": null, | |
| 10216 | + "twitch_followers": null | |
| 10217 | + }, | |
| 10218 | + "category": "médias", | |
| 10219 | + "location": "Québec", | |
| 10220 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); animatrice et productrice", | |
| 10221 | + "sources": [ | |
| 10222 | + "rythme_twitter" | |
| 10223 | + ], | |
| 10224 | + "total_followers": null, | |
| 10225 | + "platform_count": 1, | |
| 10226 | + "tier": "nano/inconnu" | |
| 10227 | + }, | |
| 10228 | + { | |
| 10229 | + "name": "Stéphane Laporte", | |
| 10230 | + "handles": { | |
| 10231 | + "instagram": null, | |
| 10232 | + "tiktok": null, | |
| 10233 | + "youtube": null, | |
| 10234 | + "facebook": null, | |
| 10235 | + "x": "laportestephane", | |
| 10236 | + "twitch": null | |
| 10237 | + }, | |
| 10238 | + "metrics": { | |
| 10239 | + "instagram_followers": null, | |
| 10240 | + "tiktok_followers": null, | |
| 10241 | + "youtube_subscribers": null, | |
| 10242 | + "facebook_followers": null, | |
| 10243 | + "x_followers": null, | |
| 10244 | + "twitch_followers": null | |
| 10245 | + }, | |
| 10246 | + "category": "médias", | |
| 10247 | + "location": "Québec", | |
| 10248 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); auteur et chroniqueur", | |
| 10249 | + "sources": [ | |
| 10250 | + "rythme_twitter" | |
| 10251 | + ], | |
| 10252 | + "total_followers": null, | |
| 10253 | + "platform_count": 1, | |
| 10254 | + "tier": "nano/inconnu" | |
| 10255 | + }, | |
| 10256 | + { | |
| 10257 | + "name": "Paul Arcand", | |
| 10258 | + "handles": { | |
| 10259 | + "instagram": null, | |
| 10260 | + "tiktok": null, | |
| 10261 | + "youtube": null, | |
| 10262 | + "facebook": null, | |
| 10263 | + "x": null, | |
| 10264 | + "twitch": null | |
| 10265 | + }, | |
| 10266 | + "metrics": { | |
| 10267 | + "instagram_followers": null, | |
| 10268 | + "tiktok_followers": null, | |
| 10269 | + "youtube_subscribers": null, | |
| 10270 | + "facebook_followers": null, | |
| 10271 | + "x_followers": null, | |
| 10272 | + "twitch_followers": null | |
| 10273 | + }, | |
| 10274 | + "category": "médias", | |
| 10275 | + "location": "Québec", | |
| 10276 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); animateur radio", | |
| 10277 | + "sources": [ | |
| 10278 | + "rythme_twitter" | |
| 10279 | + ], | |
| 10280 | + "total_followers": null, | |
| 10281 | + "platform_count": 0, | |
| 10282 | + "tier": "nano/inconnu" | |
| 10283 | + }, | |
| 10284 | + { | |
| 10285 | + "name": "Marie-Mai", | |
| 10286 | + "handles": { | |
| 10287 | + "instagram": null, | |
| 10288 | + "tiktok": null, | |
| 10289 | + "youtube": null, | |
| 10290 | + "facebook": null, | |
| 10291 | + "x": "mariemaireal", | |
| 10292 | + "twitch": null | |
| 10293 | + }, | |
| 10294 | + "metrics": { | |
| 10295 | + "instagram_followers": null, | |
| 10296 | + "tiktok_followers": null, | |
| 10297 | + "youtube_subscribers": null, | |
| 10298 | + "facebook_followers": null, | |
| 10299 | + "x_followers": null, | |
| 10300 | + "twitch_followers": null | |
| 10301 | + }, | |
| 10302 | + "category": "musique", | |
| 10303 | + "location": "Québec", | |
| 10304 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); chanteuse", | |
| 10305 | + "sources": [ | |
| 10306 | + "rythme_twitter" | |
| 10307 | + ], | |
| 10308 | + "total_followers": null, | |
| 10309 | + "platform_count": 1, | |
| 10310 | + "tier": "nano/inconnu" | |
| 10311 | + }, | |
| 10312 | + { | |
| 10313 | + "name": "Marie-France Bazzo", | |
| 10314 | + "handles": { | |
| 10315 | + "instagram": null, | |
| 10316 | + "tiktok": null, | |
| 10317 | + "youtube": null, | |
| 10318 | + "facebook": null, | |
| 10319 | + "x": null, | |
| 10320 | + "twitch": null | |
| 10321 | + }, | |
| 10322 | + "metrics": { | |
| 10323 | + "instagram_followers": null, | |
| 10324 | + "tiktok_followers": null, | |
| 10325 | + "youtube_subscribers": null, | |
| 10326 | + "facebook_followers": null, | |
| 10327 | + "x_followers": null, | |
| 10328 | + "twitch_followers": null | |
| 10329 | + }, | |
| 10330 | + "category": "médias", | |
| 10331 | + "location": "Québec", | |
| 10332 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); animatrice et productrice", | |
| 10333 | + "sources": [ | |
| 10334 | + "rythme_twitter" | |
| 10335 | + ], | |
| 10336 | + "total_followers": null, | |
| 10337 | + "platform_count": 0, | |
| 10338 | + "tier": "nano/inconnu" | |
| 10339 | + }, | |
| 10340 | + { | |
| 10341 | + "name": "Lara Fabian", | |
| 10342 | + "handles": { | |
| 10343 | + "instagram": null, | |
| 10344 | + "tiktok": null, | |
| 10345 | + "youtube": null, | |
| 10346 | + "facebook": null, | |
| 10347 | + "x": null, | |
| 10348 | + "twitch": null | |
| 10349 | + }, | |
| 10350 | + "metrics": { | |
| 10351 | + "instagram_followers": null, | |
| 10352 | + "tiktok_followers": null, | |
| 10353 | + "youtube_subscribers": null, | |
| 10354 | + "facebook_followers": null, | |
| 10355 | + "x_followers": null, | |
| 10356 | + "twitch_followers": null | |
| 10357 | + }, | |
| 10358 | + "category": "musique", | |
| 10359 | + "location": "Québec", | |
| 10360 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); chanteuse", | |
| 10361 | + "sources": [ | |
| 10362 | + "rythme_twitter" | |
| 10363 | + ], | |
| 10364 | + "total_followers": null, | |
| 10365 | + "platform_count": 0, | |
| 10366 | + "tier": "nano/inconnu" | |
| 10367 | + }, | |
| 10368 | + { | |
| 10369 | + "name": "Charles Lafortune", | |
| 10370 | + "handles": { | |
| 10371 | + "instagram": null, | |
| 10372 | + "tiktok": null, | |
| 10373 | + "youtube": null, | |
| 10374 | + "facebook": null, | |
| 10375 | + "x": "charleslaf", | |
| 10376 | + "twitch": null | |
| 10377 | + }, | |
| 10378 | + "metrics": { | |
| 10379 | + "instagram_followers": null, | |
| 10380 | + "tiktok_followers": null, | |
| 10381 | + "youtube_subscribers": null, | |
| 10382 | + "facebook_followers": null, | |
| 10383 | + "x_followers": null, | |
| 10384 | + "twitch_followers": null | |
| 10385 | + }, | |
| 10386 | + "category": "médias", | |
| 10387 | + "location": "Québec", | |
| 10388 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); animateur", | |
| 10389 | + "sources": [ | |
| 10390 | + "rythme_twitter" | |
| 10391 | + ], | |
| 10392 | + "total_followers": null, | |
| 10393 | + "platform_count": 1, | |
| 10394 | + "tier": "nano/inconnu" | |
| 10395 | + }, | |
| 10396 | + { | |
| 10397 | + "name": "Jean-Thomas Jobin", | |
| 10398 | + "handles": { | |
| 10399 | + "instagram": null, | |
| 10400 | + "tiktok": null, | |
| 10401 | + "youtube": null, | |
| 10402 | + "facebook": null, | |
| 10403 | + "x": "jeanthomasjobin", | |
| 10404 | + "twitch": null | |
| 10405 | + }, | |
| 10406 | + "metrics": { | |
| 10407 | + "instagram_followers": null, | |
| 10408 | + "tiktok_followers": null, | |
| 10409 | + "youtube_subscribers": null, | |
| 10410 | + "facebook_followers": null, | |
| 10411 | + "x_followers": null, | |
| 10412 | + "twitch_followers": null | |
| 10413 | + }, | |
| 10414 | + "category": "humour", | |
| 10415 | + "location": "Québec", | |
| 10416 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); humoriste", | |
| 10417 | + "sources": [ | |
| 10418 | + "rythme_twitter" | |
| 10419 | + ], | |
| 10420 | + "total_followers": null, | |
| 10421 | + "platform_count": 1, | |
| 10422 | + "tier": "nano/inconnu" | |
| 10423 | + }, | |
| 10424 | + { | |
| 10425 | + "name": "Benoît Dutrizac", | |
| 10426 | + "handles": { | |
| 10427 | + "instagram": null, | |
| 10428 | + "tiktok": null, | |
| 10429 | + "youtube": null, | |
| 10430 | + "facebook": null, | |
| 10431 | + "x": null, | |
| 10432 | + "twitch": null | |
| 10433 | + }, | |
| 10434 | + "metrics": { | |
| 10435 | + "instagram_followers": null, | |
| 10436 | + "tiktok_followers": null, | |
| 10437 | + "youtube_subscribers": null, | |
| 10438 | + "facebook_followers": null, | |
| 10439 | + "x_followers": null, | |
| 10440 | + "twitch_followers": null | |
| 10441 | + }, | |
| 10442 | + "category": "médias", | |
| 10443 | + "location": "Québec", | |
| 10444 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); animateur et chroniqueur", | |
| 10445 | + "sources": [ | |
| 10446 | + "rythme_twitter" | |
| 10447 | + ], | |
| 10448 | + "total_followers": null, | |
| 10449 | + "platform_count": 0, | |
| 10450 | + "tier": "nano/inconnu" | |
| 10451 | + }, | |
| 10452 | + { | |
| 10453 | + "name": "Mario Dumont", | |
| 10454 | + "handles": { | |
| 10455 | + "instagram": null, | |
| 10456 | + "tiktok": null, | |
| 10457 | + "youtube": null, | |
| 10458 | + "facebook": null, | |
| 10459 | + "x": null, | |
| 10460 | + "twitch": null | |
| 10461 | + }, | |
| 10462 | + "metrics": { | |
| 10463 | + "instagram_followers": null, | |
| 10464 | + "tiktok_followers": null, | |
| 10465 | + "youtube_subscribers": null, | |
| 10466 | + "facebook_followers": null, | |
| 10467 | + "x_followers": null, | |
| 10468 | + "twitch_followers": null | |
| 10469 | + }, | |
| 10470 | + "category": "médias", | |
| 10471 | + "location": "Québec", | |
| 10472 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); animateur, ex-politicien", | |
| 10473 | + "sources": [ | |
| 10474 | + "rythme_twitter" | |
| 10475 | + ], | |
| 10476 | + "total_followers": null, | |
| 10477 | + "platform_count": 0, | |
| 10478 | + "tier": "nano/inconnu" | |
| 10479 | + }, | |
| 10480 | + { | |
| 10481 | + "name": "Patrick Groulx", | |
| 10482 | + "handles": { | |
| 10483 | + "instagram": null, | |
| 10484 | + "tiktok": null, | |
| 10485 | + "youtube": null, | |
| 10486 | + "facebook": null, | |
| 10487 | + "x": null, | |
| 10488 | + "twitch": null | |
| 10489 | + }, | |
| 10490 | + "metrics": { | |
| 10491 | + "instagram_followers": null, | |
| 10492 | + "tiktok_followers": null, | |
| 10493 | + "youtube_subscribers": null, | |
| 10494 | + "facebook_followers": null, | |
| 10495 | + "x_followers": null, | |
| 10496 | + "twitch_followers": null | |
| 10497 | + }, | |
| 10498 | + "category": "humour", | |
| 10499 | + "location": "Québec", | |
| 10500 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); humoriste", | |
| 10501 | + "sources": [ | |
| 10502 | + "rythme_twitter" | |
| 10503 | + ], | |
| 10504 | + "total_followers": null, | |
| 10505 | + "platform_count": 0, | |
| 10506 | + "tier": "nano/inconnu" | |
| 10507 | + }, | |
| 10508 | + { | |
| 10509 | + "name": "MC Gilles", | |
| 10510 | + "handles": { | |
| 10511 | + "instagram": null, | |
| 10512 | + "tiktok": null, | |
| 10513 | + "youtube": null, | |
| 10514 | + "facebook": null, | |
| 10515 | + "x": "mcgilles", | |
| 10516 | + "twitch": null | |
| 10517 | + }, | |
| 10518 | + "metrics": { | |
| 10519 | + "instagram_followers": null, | |
| 10520 | + "tiktok_followers": null, | |
| 10521 | + "youtube_subscribers": null, | |
| 10522 | + "facebook_followers": null, | |
| 10523 | + "x_followers": null, | |
| 10524 | + "twitch_followers": null | |
| 10525 | + }, | |
| 10526 | + "category": "médias", | |
| 10527 | + "location": "Québec", | |
| 10528 | + "notes": "Top 25 Twitter QC 2019 (>120K abonnés); chroniqueur et DJ", | |
| 10529 | + "sources": [ | |
| 10530 | + "rythme_twitter" | |
| 10531 | + ], | |
| 10532 | + "total_followers": null, | |
| 10533 | + "platform_count": 1, | |
| 10534 | + "tier": "nano/inconnu" | |
| 10535 | + }, | |
| 10536 | + { | |
| 10537 | + "name": "Garou", | |
| 10538 | + "handles": { | |
| 10539 | + "instagram": null, | |
| 10540 | + "tiktok": null, | |
| 10541 | + "youtube": null, | |
| 10542 | + "facebook": null, | |
| 10543 | + "x": null, | |
| 10544 | + "twitch": null | |
| 10545 | + }, | |
| 10546 | + "metrics": { | |
| 10547 | + "instagram_followers": null, | |
| 10548 | + "tiktok_followers": null, | |
| 10549 | + "youtube_subscribers": null, | |
| 10550 | + "facebook_followers": null, | |
| 10551 | + "x_followers": null, | |
| 10552 | + "twitch_followers": null | |
| 10553 | + }, | |
| 10554 | + "category": "musique", | |
| 10555 | + "location": "Québec", | |
| 10556 | + "notes": "3e du top Twitter QC 2018, retiré du palmarès 2019 (ne vivait plus principalement au Québec)", | |
| 10557 | + "sources": [ | |
| 10558 | + "rythme_twitter" | |
| 10559 | + ], | |
| 10560 | + "total_followers": null, | |
| 10561 | + "platform_count": 0, | |
| 10562 | + "tier": "nano/inconnu" | |
| 10563 | + }, | |
| 10564 | + { | |
| 10565 | + "name": "Martin Rego", | |
| 10566 | + "handles": { | |
| 10567 | + "instagram": null, | |
| 10568 | + "tiktok": null, | |
| 10569 | + "youtube": null, | |
| 10570 | + "facebook": null, | |
| 10571 | + "x": "martin_rego", | |
| 10572 | + "twitch": null | |
| 10573 | + }, | |
| 10574 | + "metrics": { | |
| 10575 | + "instagram_followers": null, | |
| 10576 | + "tiktok_followers": null, | |
| 10577 | + "youtube_subscribers": null, | |
| 10578 | + "facebook_followers": null, | |
| 10579 | + "x_followers": null, | |
| 10580 | + "twitch_followers": null | |
| 10581 | + }, | |
| 10582 | + "category": "médias", | |
| 10583 | + "location": "Québec", | |
| 10584 | + "notes": "Animateur et youtubeur, auteur du palmarès annuel des personnalités QC sur Twitter", | |
| 10585 | + "sources": [ | |
| 10586 | + "rythme_twitter" | |
| 10587 | + ], | |
| 10588 | + "total_followers": null, | |
| 10589 | + "platform_count": 1, | |
| 10590 | + "tier": "nano/inconnu" | |
| 10591 | + }, | |
| 10592 | + { | |
| 10593 | + "name": "Le Rêve d'un Vagabond", | |
| 10594 | + "handles": { | |
| 10595 | + "instagram": null, | |
| 10596 | + "tiktok": null, | |
| 10597 | + "youtube": null, | |
| 10598 | + "facebook": "revedunvagabond", | |
| 10599 | + "x": null, | |
| 10600 | + "twitch": null | |
| 10601 | + }, | |
| 10602 | + "metrics": { | |
| 10603 | + "instagram_followers": null, | |
| 10604 | + "tiktok_followers": null, | |
| 10605 | + "youtube_subscribers": null, | |
| 10606 | + "facebook_followers": null, | |
| 10607 | + "x_followers": null, | |
| 10608 | + "twitch_followers": null | |
| 10609 | + }, | |
| 10610 | + "category": "voyage", | |
| 10611 | + "location": "Québec", | |
| 10612 | + "notes": "Québécois digital nomade, vidéos du quotidien d'un voyageur sur Facebook; #4 du top VoyageurQc", | |
| 10613 | + "sources": [ | |
| 10614 | + "voyageurqc" | |
| 10615 | + ], | |
| 10616 | + "total_followers": null, | |
| 10617 | + "platform_count": 1, | |
| 10618 | + "tier": "nano/inconnu" | |
| 10619 | + }, | |
| 10620 | + { | |
| 10621 | + "name": "Rim Hajri", | |
| 10622 | + "handles": { | |
| 10623 | + "instagram": "rimhajri", | |
| 10624 | + "tiktok": null, | |
| 10625 | + "youtube": null, | |
| 10626 | + "facebook": null, | |
| 10627 | + "x": null, | |
| 10628 | + "twitch": null | |
| 10629 | + }, | |
| 10630 | + "metrics": { | |
| 10631 | + "instagram_followers": null, | |
| 10632 | + "tiktok_followers": null, | |
| 10633 | + "youtube_subscribers": null, | |
| 10634 | + "facebook_followers": null, | |
| 10635 | + "x_followers": null, | |
| 10636 | + "twitch_followers": null | |
| 10637 | + }, | |
| 10638 | + "category": "voyage", | |
| 10639 | + "location": "Montréal", | |
| 10640 | + "notes": "Blogueuse Where It's Blue (plongée, îles); #2 du top VoyageurQc", | |
| 10641 | + "sources": [ | |
| 10642 | + "voyageurqc" | |
| 10643 | + ], | |
| 10644 | + "total_followers": null, | |
| 10645 | + "platform_count": 1, | |
| 10646 | + "tier": "nano/inconnu" | |
| 10647 | + }, | |
| 10648 | + { | |
| 10649 | + "name": "Steeven (Voyageur.Qc / Minion)", | |
| 10650 | + "handles": { | |
| 10651 | + "instagram": "voyageur.qc", | |
| 10652 | + "tiktok": null, | |
| 10653 | + "youtube": null, | |
| 10654 | + "facebook": null, | |
| 10655 | + "x": null, | |
| 10656 | + "twitch": null | |
| 10657 | + }, | |
| 10658 | + "metrics": { | |
| 10659 | + "instagram_followers": null, | |
| 10660 | + "tiktok_followers": null, | |
| 10661 | + "youtube_subscribers": null, | |
| 10662 | + "facebook_followers": null, | |
| 10663 | + "x_followers": null, | |
| 10664 | + "twitch_followers": null | |
| 10665 | + }, | |
| 10666 | + "category": "voyage", | |
| 10667 | + "location": "Québec", | |
| 10668 | + "notes": "Auteur du blogue VoyageurQc; met en scène un Minion dans ses photos de voyage; #1 de son top", | |
| 10669 | + "sources": [ | |
| 10670 | + "voyageurqc" | |
| 10671 | + ], | |
| 10672 | + "total_followers": null, | |
| 10673 | + "platform_count": 1, | |
| 10674 | + "tier": "nano/inconnu" | |
| 10675 | + }, | |
| 10676 | + { | |
| 10677 | + "name": "Alex & MJ on the go", | |
| 10678 | + "handles": { | |
| 10679 | + "instagram": null, | |
| 10680 | + "tiktok": null, | |
| 10681 | + "youtube": null, | |
| 10682 | + "facebook": null, | |
| 10683 | + "x": null, | |
| 10684 | + "twitch": null | |
| 10685 | + }, | |
| 10686 | + "metrics": { | |
| 10687 | + "instagram_followers": null, | |
| 10688 | + "tiktok_followers": null, | |
| 10689 | + "youtube_subscribers": null, | |
| 10690 | + "facebook_followers": null, | |
| 10691 | + "x_followers": null, | |
| 10692 | + "twitch_followers": null | |
| 10693 | + }, | |
| 10694 | + "category": "voyage", | |
| 10695 | + "location": "Québec", | |
| 10696 | + "notes": "Couple de voyageurs québécois mentionné dans l'article VoyageurQc", | |
| 10697 | + "sources": [ | |
| 10698 | + "voyageurqc" | |
| 10699 | + ], | |
| 10700 | + "total_followers": null, | |
| 10701 | + "platform_count": 0, | |
| 10702 | + "tier": "nano/inconnu" | |
| 10703 | + }, | |
| 10704 | + { | |
| 10705 | + "name": "Mathieu Dupuis", | |
| 10706 | + "handles": { | |
| 10707 | + "instagram": "mathieu_dupuis_photo", | |
| 10708 | + "tiktok": null, | |
| 10709 | + "youtube": null, | |
| 10710 | + "facebook": null, | |
| 10711 | + "x": null, | |
| 10712 | + "twitch": null | |
| 10713 | + }, | |
| 10714 | + "metrics": { | |
| 10715 | + "instagram_followers": null, | |
| 10716 | + "tiktok_followers": null, | |
| 10717 | + "youtube_subscribers": null, | |
| 10718 | + "facebook_followers": null, | |
| 10719 | + "x_followers": null, | |
| 10720 | + "twitch_followers": null | |
| 10721 | + }, | |
| 10722 | + "category": "voyage", | |
| 10723 | + "location": "Québec", | |
| 10724 | + "notes": "Photographe de voyage sillonnant les routes en Sprinter 4x4 (aussi @matdupuisphoto)", | |
| 10725 | + "sources": [ | |
| 10726 | + "noovomoi_voyage" | |
| 10727 | + ], | |
| 10728 | + "total_followers": null, | |
| 10729 | + "platform_count": 1, | |
| 10730 | + "tier": "nano/inconnu" | |
| 10731 | + } | |
| 10732 | +] | |
| \ No newline at end of file | ||
added
data/sources.json
+98 −0
@@ -0,0 +1,98 @@ | ||
| 1 | +{ | |
| 2 | + "_author": "Simon-Pierre Boucher <contact@spboucher.ai>", | |
| 3 | + "_file": "data/sources.json", | |
| 4 | + "_desc": "Registre des sources connectées — famille, mode d'accès, cadence (CLAUDE.md §9, §14, §19)", | |
| 5 | + "updated": "2026-08-17", | |
| 6 | + "sources": [ | |
| 7 | + { | |
| 8 | + "id": "listes-medias", | |
| 9 | + "famille": "decouverte", | |
| 10 | + "palier": 3, | |
| 11 | + "acces": "dataset local (listes & palmarès médias publics compilés : billie, frank, metricool, Favikon QC, canadult…)", | |
| 12 | + "signal_identite": "listes_medias (0.85)", | |
| 13 | + "cadence": "mensuelle (recompilation du dataset)", | |
| 14 | + "notes": "376 créateurs québécois, handles par plateforme + audiences approximatives" | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + "id": "wikidata-qc", | |
| 18 | + "famille": "decouverte", | |
| 19 | + "palier": 3, | |
| 20 | + "acces": "API SPARQL publique Wikidata (nés/résidant au Québec, occupation créateur, handles sociaux curés)", | |
| 21 | + "signal_identite": "base_publique (0.85)", | |
| 22 | + "cadence": "hebdomadaire", | |
| 23 | + "notes": "humoristes, youtubeurs, vidéastes web, streamers, podcasteurs notables ; is_minor levé via l'année de naissance publique" | |
| 24 | + }, | |
| 25 | + { | |
| 26 | + "id": "onlyqueb", | |
| 27 | + "famille": "decouverte", | |
| 28 | + "palier": 3, | |
| 29 | + "acces": "requêtes directes (sitemap.xml + JSON-LD ProfilePage des pages profil publiques SSR)", | |
| 30 | + "signal_identite": "cross_link (0.90) — liens sameAs auto-déclarés par la personne", | |
| 31 | + "cadence": "hebdomadaire", | |
| 32 | + "notes": "annuaire public onlyqueb.com — créateurs québécois sur abonnement (18+) ; nom public, photo, liens OnlyFans/Instagram/TikTok/X" | |
| 33 | + }, | |
| 34 | + { | |
| 35 | + "id": "youtube-recherche", | |
| 36 | + "famille": "decouverte", | |
| 37 | + "palier": 3, | |
| 38 | + "acces": "requêtes directes (recherche publique YouTube filtre « chaînes », ytInitialData) — ~120 requêtes thématiques × marqueurs QC", | |
| 39 | + "signal_identite": "profil_source (0.98) — la chaîne est le compte d'origine de la fiche", | |
| 40 | + "cadence": "quotidienne (watch) — la couverture s'accumule à chaque passage", | |
| 41 | + "notes": "filtre conservateur : marqueur québécois requis dans titre/description de la chaîne" | |
| 42 | + }, | |
| 43 | + { | |
| 44 | + "id": "balados-itunes", | |
| 45 | + "famille": "decouverte", | |
| 46 | + "palier": 2, | |
| 47 | + "acces": "API de recherche iTunes/Apple Podcasts (officielle, gratuite, sans clé)", | |
| 48 | + "signal_identite": "profil_source (0.98)", | |
| 49 | + "cadence": "quotidienne (watch)", | |
| 50 | + "notes": "podcasteurs québécois : nom, artiste, pochette, genre, nb d'épisodes, flux RSS ; marqueur QC requis" | |
| 51 | + }, | |
| 52 | + { | |
| 53 | + "id": "instagram-profil", | |
| 54 | + "famille": "enrichissement", | |
| 55 | + "palier": 4, | |
| 56 | + "acces": "Scrapfly (profil public web_profile_info — pas d'API publique de lecture)", | |
| 57 | + "signal_identite": "cross_link (0.90) pour les liens de bio découverts", | |
| 58 | + "cadence": "hebdomadaire (gros créateurs) / mensuelle", | |
| 59 | + "notes": "abonnés, badge, bio, liens de bio → link_in_bio_url + comptes croisés" | |
| 60 | + }, | |
| 61 | + { | |
| 62 | + "id": "tiktok-profil", | |
| 63 | + "famille": "enrichissement", | |
| 64 | + "palier": 4, | |
| 65 | + "acces": "Scrapfly (JSON d'hydratation de la page publique @handle)", | |
| 66 | + "signal_identite": "cross_link (0.90) pour le lien de bio", | |
| 67 | + "cadence": "hebdomadaire (gros créateurs) / mensuelle", | |
| 68 | + "notes": "abonnés, badge, bio, lien de bio" | |
| 69 | + }, | |
| 70 | + { | |
| 71 | + "id": "youtube", | |
| 72 | + "famille": "enrichissement", | |
| 73 | + "palier": 2, | |
| 74 | + "acces": "YouTube Data API v3 (officielle) si YOUTUBE_API_KEY, sinon page publique /@handle", | |
| 75 | + "signal_identite": "api_officielle (0.95) quand confirmé par l'API", | |
| 76 | + "cadence": "hebdomadaire", | |
| 77 | + "notes": "abonnés par chaîne" | |
| 78 | + }, | |
| 79 | + { | |
| 80 | + "id": "twitch", | |
| 81 | + "famille": "enrichissement", | |
| 82 | + "palier": 2, | |
| 83 | + "acces": "API officielle Helix (client_credentials) — sauté sans clés", | |
| 84 | + "signal_identite": "api_officielle (0.95)", | |
| 85 | + "cadence": "hebdomadaire", | |
| 86 | + "notes": "existence, type de diffuseur, description" | |
| 87 | + }, | |
| 88 | + { | |
| 89 | + "id": "link-in-bio", | |
| 90 | + "famille": "enrichissement", | |
| 91 | + "palier": 1, | |
| 92 | + "acces": "requêtes directes (pages publiques SSR : linktr.ee, beacons.ai, bio.link, msha.ke, campsite.bio, snipfeed.co), repli Firecrawl", | |
| 93 | + "signal_identite": "link_in_bio (0.95) — le signal le plus fort (§12.1)", | |
| 94 | + "cadence": "mensuelle", | |
| 95 | + "notes": "une page → TOUS les comptes du créateur ; consomme les link_in_bio_url découvertes par instagram-profil/tiktok-profil" | |
| 96 | + } | |
| 97 | + ] | |
| 98 | +} | |
added
docs/screenshots/accueil.png
+0 −0
Binary file not shown.
added
docs/screenshots/createur.png
+0 −0
Binary file not shown.
added
docs/screenshots/mobile.png
+0 −0
Binary file not shown.
added
docs/screenshots/stats.png
+0 −0
Binary file not shown.
added
frontend/dist/index.html
+695 −0
@@ -0,0 +1,695 @@ | ||
| 1 | +<!-- | |
| 2 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | + File: frontend/src/index.template.html | |
| 4 | + Desc: Gabarit du frontend Créa-Ka — {"_author":"Simon-Pierre Boucher <contact@spboucher.ai>","_file":"frontend/src/icons.json","_desc":"Trac\u00e9s SVG des marques de plateformes (simple-icons v13, CC0) + glyphes maison (linkedin/site-web/autre)","instagram":"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077","tiktok":"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z","youtube":"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z","twitch":"M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z","kick":"M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z","x":"M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z","facebook":"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z","snapchat":"M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z","substack":"M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z","patreon":"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z","onlyfans":"M24 4.003h-4.015c-3.45 0-5.3.197-6.748 1.957a7.996 7.996 0 1 0 2.103 9.211c3.182-.231 5.39-2.134 6.085-5.173 0 0-2.399.585-4.43 0 4.018-.777 6.333-3.037 7.005-5.995zM5.61 11.999A2.391 2.391 0 0 1 9.28 9.97a2.966 2.966 0 0 1 2.998-2.528h.008c-.92 1.778-1.407 3.352-1.998 5.263A2.392 2.392 0 0 1 5.61 12Zm2.386-7.996a7.996 7.996 0 1 0 7.996 7.996 7.996 7.996 0 0 0-7.996-7.996Zm0 10.394A2.399 2.399 0 1 1 10.395 12a2.396 2.396 0 0 1-2.399 2.398Z","threads":"M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z","podcast":"M5.34 0A5.328 5.328 0 000 5.34v13.32A5.328 5.328 0 005.34 24h13.32A5.328 5.328 0 0024 18.66V5.34A5.328 5.328 0 0018.66 0zm6.525 2.568c2.336 0 4.448.902 6.056 2.587 1.224 1.272 1.912 2.619 2.264 4.392.12.59.12 2.2.007 2.864a8.506 8.506 0 01-3.24 5.296c-.608.46-2.096 1.261-2.336 1.261-.088 0-.096-.091-.056-.46.072-.592.144-.715.48-.856.536-.224 1.448-.874 2.008-1.435a7.644 7.644 0 002.008-3.536c.208-.824.184-2.656-.048-3.504-.728-2.696-2.928-4.792-5.624-5.352-.784-.16-2.208-.16-3 0-2.728.56-4.984 2.76-5.672 5.528-.184.752-.184 2.584 0 3.336.456 1.832 1.64 3.512 3.192 4.512.304.2.672.408.824.472.336.144.408.264.472.856.04.36.03.464-.056.464-.056 0-.464-.176-.896-.384l-.04-.03c-2.472-1.216-4.056-3.274-4.632-6.012-.144-.706-.168-2.392-.03-3.04.36-1.74 1.048-3.1 2.192-4.304 1.648-1.737 3.768-2.656 6.128-2.656zm.134 2.81c.409.004.803.04 1.106.106 2.784.62 4.76 3.408 4.376 6.174-.152 1.114-.536 2.03-1.216 2.88-.336.43-1.152 1.15-1.296 1.15-.023 0-.048-.272-.048-.603v-.605l.416-.496c1.568-1.878 1.456-4.502-.256-6.224-.664-.67-1.432-1.064-2.424-1.246-.64-.118-.776-.118-1.448-.008-1.02.167-1.81.562-2.512 1.256-1.72 1.704-1.832 4.342-.264 6.222l.413.496v.608c0 .336-.027.608-.06.608-.03 0-.264-.16-.512-.36l-.034-.011c-.832-.664-1.568-1.842-1.872-2.997-.184-.698-.184-2.024.008-2.72.504-1.878 1.888-3.335 3.808-4.019.41-.145 1.133-.22 1.814-.211zm-.13 2.99c.31 0 .62.06.844.178.488.253.888.745 1.04 1.259.464 1.578-1.208 2.96-2.72 2.254h-.015c-.712-.331-1.096-.956-1.104-1.77 0-.733.408-1.371 1.112-1.745.224-.117.534-.176.844-.176zm-.011 4.728c.988-.004 1.706.349 1.97.97.198.464.124 1.932-.218 4.302-.232 1.656-.36 2.074-.68 2.356-.44.39-1.064.498-1.656.288h-.003c-.716-.257-.87-.605-1.164-2.644-.341-2.37-.416-3.838-.218-4.302.262-.616.974-.966 1.97-.97z","linktree":"m13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z","linkedin":"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z","site-web":"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm7.938 9h-3.243a15.6 15.6 0 00-1.4-4.653A8.03 8.03 0 0119.938 9zM12 2.05c.9 1.2 1.9 3.2 2.5 6.95h-5C10.1 5.25 11.1 3.25 12 2.05zM4.062 15A8.06 8.06 0 013.75 12c0-1.05.15-2.05.312-3h3.55A25 25 0 007.5 12c0 1.05.05 2.05.113 3zm.643 2h3.243c.35 1.8.85 3.35 1.4 4.653A8.03 8.03 0 014.705 17zm3.243-10H4.705a8.03 8.03 0 014.643-4.653A15.6 15.6 0 007.948 7zM12 21.95c-.9-1.2-1.9-3.2-2.5-6.95h5c-.6 3.75-1.6 5.75-2.5 6.95zM14.787 13H9.213A23 23 0 019.1 12c0-1.05.037-2.05.113-3h5.574c.076.95.113 1.95.113 3s-.037 2.05-.113 3zm.508 8.653c.55-1.303 1.05-2.853 1.4-4.653h3.243a8.03 8.03 0 01-4.643 4.653zM16.388 15c.062-.95.112-1.95.112-3s-.05-2.05-.112-3h3.55c.162.95.312 1.95.312 3s-.15 2.05-.312 3z","autre":"M10.59 13.41a1 1 0 010-1.41l2.83-2.83a3 3 0 114.24 4.24l-1.41 1.42a1 1 0 11-1.42-1.42l1.42-1.41a1 1 0 10-1.42-1.42L12 13.41a1 1 0 01-1.41 0zm2.82-2.82a1 1 0 010 1.41l-2.82 2.83a3 3 0 11-4.25-4.24l1.42-1.42a1 1 0 111.41 1.42L7.76 12a1 1 0 101.41 1.41L12 10.59a1 1 0 011.41 0z"} est remplacé par les tracés | |
| 5 | + SVG des marques (simple-icons, CC0) via scripts/build_frontend.py. | |
| 6 | + Système de design « éditorial sharp » du Groupe KA (identité partagée | |
| 7 | + avec groupe-ka.com, Vrai-Prix, ValoPlex : papier grainé #f5f3ee, encre | |
| 8 | + #141814, bordures encre + ombres décalées dures, Space Grotesk / Inter | |
| 9 | + / JetBrains Mono) — accent propre Créa-Ka : VIOLET ÉLECTRIQUE. | |
| 10 | + Patrons mobiles calqués sur Lou-Ka (tab bar 4 onglets, filtres | |
| 11 | + défilants, anti-zoom iOS). Routes : / · /createur/{id} · /stats · /retrait | |
| 12 | +--> | |
| 13 | +<!DOCTYPE html> | |
| 14 | +<html lang="fr"> | |
| 15 | +<head> | |
| 16 | +<meta charset="utf-8"> | |
| 17 | +<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | |
| 18 | +<title>Créa-Ka — Tous les créateurs québécois. Tous leurs liens.</title> | |
| 19 | +<meta name="description" content="Annuaire public des créateurs de contenu et influenceurs québécois : tous leurs comptes (Instagram, TikTok, YouTube, Twitch…) regroupés, cherchables par niche, région, langue et taille d'audience."> | |
| 20 | +<meta name="theme-color" content="#141814"> | |
| 21 | +<link rel="preconnect" href="https://fonts.googleapis.com"> | |
| 22 | +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| 23 | +<link rel="preconnect" href="https://unavatar.io"> | |
| 24 | +<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet"> | |
| 25 | +<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%23141814'/%3E%3Ccircle cx='32' cy='23' r='9' fill='%23c9a3ff'/%3E%3Cpath d='M14 52c3-10 9-15 18-15s15 5 18 15' stroke='%23c9a3ff' stroke-width='6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"> | |
| 26 | +<style> | |
| 27 | +/* système « éditorial sharp » Groupe KA — accent Créa-Ka : violet électrique. | |
| 28 | + Convention du groupe : les NOMS de variables (--green/--lime) sont partagés | |
| 29 | + entre les apps, seules les VALEURS changent. */ | |
| 30 | +:root{ | |
| 31 | + --paper:#f5f3ee; --surface:#fff; --surface-2:#faf9f5; | |
| 32 | + --ink:#141814; --ink-2:#4d5551; --ink-3:#8b928c; | |
| 33 | + --line:rgba(20,24,20,.14); --line-strong:rgba(20,24,20,.85); | |
| 34 | + --green:#5b34a8; /* violet profond (primaire) */ | |
| 35 | + --green-deep:#3d2178; | |
| 36 | + --lime:#c9a3ff; /* violet électrique (accent) */ | |
| 37 | + --lime-soft:#efe6ff; | |
| 38 | + --amber:#e8a33d; --amber-soft:#fdf3e2; | |
| 39 | + --danger:#b3423a; | |
| 40 | + --r-card:10px; --r-ctl:6px; --r-pill:999px; | |
| 41 | + --shadow-flat:0 1px 2px rgba(20,24,20,.05); | |
| 42 | + --shadow-off:6px 6px 0 var(--ink); | |
| 43 | + --shadow-off-soft:8px 8px 0 rgba(20,24,20,.08); | |
| 44 | + --font-display:"Space Grotesk",system-ui,sans-serif; | |
| 45 | + --font-body:"Inter",system-ui,sans-serif; | |
| 46 | + --font-mono:"JetBrains Mono",ui-monospace,monospace; | |
| 47 | +} | |
| 48 | +*{box-sizing:border-box} | |
| 49 | +html{scroll-behavior:smooth} | |
| 50 | +body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font-body); | |
| 51 | + font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased; | |
| 52 | + padding-bottom:env(safe-area-inset-bottom)} | |
| 53 | +/* grain de film pleine page (signature du groupe) — aucun z-index sur body>* */ | |
| 54 | +body::before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.35;z-index:9999; | |
| 55 | + background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E")} | |
| 56 | +body>*{position:relative} | |
| 57 | +h1,h2,h3,h4{font-family:var(--font-display);letter-spacing:-.03em;margin:0} | |
| 58 | +a{color:inherit;text-decoration:none} button{font-family:inherit;cursor:pointer} img{display:block} | |
| 59 | +::selection{background:var(--lime);color:var(--ink)} | |
| 60 | +:focus-visible{outline:2px solid var(--green);outline-offset:2px} | |
| 61 | +.icn{width:1em;height:1em;display:block;fill:currentColor} | |
| 62 | +.container{max-width:1240px;margin:0 auto;padding:0 24px} | |
| 63 | +@media(max-width:640px){.container{padding:0 16px}} | |
| 64 | +.kicker{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono); | |
| 65 | + font-size:11.5px;font-weight:500;text-transform:uppercase;letter-spacing:.12em;color:var(--green)} | |
| 66 | +.kicker::before{content:"";width:22px;height:2px;background:var(--green)} | |
| 67 | +.klabel{font-family:var(--font-mono);font-size:10px;font-weight:700; | |
| 68 | + text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3)} | |
| 69 | +.hl{display:inline-block;background:var(--lime);border-radius:8px;padding:0 10px 2px;transform:rotate(-1deg)} | |
| 70 | +.outline-txt{color:transparent;-webkit-text-stroke:2px var(--ink)} | |
| 71 | + | |
| 72 | +/* ---------- cartes & boutons (langage du groupe) ---------- */ | |
| 73 | +.gk-card{background:var(--surface);border:1.5px solid var(--ink); | |
| 74 | + border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)} | |
| 75 | +.gk-hover{transition:transform .18s ease,box-shadow .18s ease} | |
| 76 | +.gk-hover:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)} | |
| 77 | +.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px; | |
| 78 | + padding:10px 20px;border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 79 | + font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer; | |
| 80 | + transition:background .15s ease,transform .1s ease,box-shadow .1s ease} | |
| 81 | +.btn:active{transform:translate(2px,2px);box-shadow:none!important} | |
| 82 | +.btn-primary{background:var(--ink);color:var(--lime);box-shadow:4px 4px 0 rgba(20,24,20,.25)} | |
| 83 | +.btn-primary:hover{background:var(--green-deep)} | |
| 84 | +.btn-ghost{background:transparent;color:var(--ink)} | |
| 85 | +.btn-ghost:hover{background:var(--lime)} | |
| 86 | + | |
| 87 | +/* ===== header ===== */ | |
| 88 | +.header{position:sticky;top:0;z-index:50;background:rgba(245,243,238,.9); | |
| 89 | + backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1.5px solid var(--ink)} | |
| 90 | +.header-inner{display:flex;align-items:center;gap:16px;height:64px} | |
| 91 | +.brand{font-family:var(--font-display);font-weight:700;font-size:23px;letter-spacing:-.03em; | |
| 92 | + display:flex;align-items:center;line-height:1;color:var(--ink)} | |
| 93 | +.brand svg{margin-right:11px;flex:none} | |
| 94 | +.brand .ka{background:var(--lime);border-radius:6px;padding:0 6px 1px;margin-left:2px;transform:rotate(-1deg)} | |
| 95 | +.brand-tag{font-family:var(--font-mono);font-size:10px;font-weight:500;color:var(--ink-3); | |
| 96 | + line-height:1.5;text-transform:uppercase;letter-spacing:.08em} | |
| 97 | +@media(max-width:900px){.brand-tag{display:none}} | |
| 98 | +.header-spacer{flex:1} | |
| 99 | +.hbtn{border:1.5px solid var(--ink);background:var(--surface);color:var(--ink); | |
| 100 | + border-radius:var(--r-ctl);padding:8px 16px;font-family:var(--font-display); | |
| 101 | + font-size:13px;font-weight:700;transition:.15s;display:inline-flex;align-items:center; | |
| 102 | + gap:8px;white-space:nowrap;box-shadow:3px 3px 0 rgba(20,24,20,.12)} | |
| 103 | +.hbtn:hover{background:var(--lime)} | |
| 104 | +.hbtn:active{transform:translate(2px,2px);box-shadow:none} | |
| 105 | +.hbtn.primary{background:var(--ink);color:var(--lime)} | |
| 106 | +.hbtn.primary:hover{background:var(--green-deep)} | |
| 107 | +@media(max-width:760px){.header .hbtn{display:none}} | |
| 108 | + | |
| 109 | +/* ===== héros ===== */ | |
| 110 | +.hero{padding:54px 0 26px} | |
| 111 | +@media(max-width:640px){.hero{padding:30px 0 12px}} | |
| 112 | +.hero h1{font-size:clamp(30px,5.4vw,54px);font-weight:700;max-width:820px;line-height:1.06; | |
| 113 | + text-transform:uppercase} | |
| 114 | +.hero p.lead{color:var(--ink-2);font-size:clamp(14.5px,2vw,16.5px);max-width:640px;margin:18px 0 0} | |
| 115 | +.stats-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px} | |
| 116 | +.stat-pill{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 117 | + padding:8px 16px;font-family:var(--font-mono);font-size:11px;font-weight:700; | |
| 118 | + text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2); | |
| 119 | + display:inline-flex;gap:8px;align-items:center;box-shadow:3px 3px 0 rgba(20,24,20,.1)} | |
| 120 | +.stat-pill b{font-family:var(--font-display);color:var(--ink);font-size:16px;letter-spacing:0} | |
| 121 | +.advantages{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:32px 0 8px} | |
| 122 | +@media(max-width:900px){.advantages{grid-template-columns:repeat(2,1fr);gap:10px}} | |
| 123 | +.adv{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 124 | + padding:16px;box-shadow:var(--shadow-off-soft)} | |
| 125 | +.adv .ico{width:36px;height:36px;border-radius:8px;border:1.5px solid var(--ink); | |
| 126 | + background:var(--lime);color:var(--ink);display:flex;align-items:center; | |
| 127 | + justify-content:center;margin-bottom:10px;font-size:15px} | |
| 128 | +.adv:nth-child(2n) .ico{background:var(--amber-soft)} | |
| 129 | +.adv h3{font-size:14px;margin-bottom:3px} | |
| 130 | +.adv p{margin:0;font-size:12px;color:var(--ink-3);line-height:1.45} | |
| 131 | + | |
| 132 | +/* ===== recherche + filtres ===== */ | |
| 133 | +.searchbar{position:sticky;top:64px;z-index:40;background:rgba(245,243,238,.94); | |
| 134 | + backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px); | |
| 135 | + padding:12px 0;border-bottom:1.5px solid var(--ink)} | |
| 136 | +.search-wrap{display:flex;gap:10px;align-items:center} | |
| 137 | +.search-input{flex:1 1 auto;display:flex;align-items:center;gap:10px;background:var(--surface); | |
| 138 | + border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:10px 15px; | |
| 139 | + transition:.15s;min-width:0;box-shadow:3px 3px 0 rgba(20,24,20,.08)} | |
| 140 | +.search-input:focus-within{box-shadow:3px 3px 0 rgba(91,52,168,.3)} | |
| 141 | +.search-input input{border:0;background:none;outline:none;font:inherit;flex:1;color:var(--ink);min-width:0} | |
| 142 | +.search-input svg{flex:none;color:var(--ink-3)} | |
| 143 | +.filters{display:flex;gap:8px;flex-wrap:wrap} | |
| 144 | +select.f{appearance:none;-webkit-appearance:none;border:1.5px solid var(--ink); | |
| 145 | + background:var(--surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23141814' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 12px center; | |
| 146 | + border-radius:var(--r-ctl);padding:9px 30px 9px 14px;font-family:var(--font-display); | |
| 147 | + font-size:13px;font-weight:700;color:var(--ink-2);max-width:170px; | |
| 148 | + box-shadow:3px 3px 0 rgba(20,24,20,.08)} | |
| 149 | +select.f:focus{outline:2px solid var(--green);outline-offset:1px} | |
| 150 | +select.f.active{background-color:var(--lime);color:var(--ink)} | |
| 151 | +@media(max-width:760px){ | |
| 152 | + .search-wrap{flex-wrap:wrap} | |
| 153 | + .search-input{flex-basis:100%} | |
| 154 | + .search-input input{font-size:16px} | |
| 155 | + .filters{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch; | |
| 156 | + margin:0 -16px;padding:4px 16px 6px;scrollbar-width:none;width:calc(100% + 32px)} | |
| 157 | + .filters::-webkit-scrollbar{display:none} | |
| 158 | + select.f{flex:none;font-size:16px;padding:8px 30px 8px 13px} | |
| 159 | +} | |
| 160 | + | |
| 161 | +/* ===== grille créateurs ===== */ | |
| 162 | +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:18px;padding:24px 0 10px} | |
| 163 | +@media(max-width:640px){.grid{grid-template-columns:repeat(2,1fr);gap:12px}} | |
| 164 | +@media(max-width:420px){.grid{grid-template-columns:1fr;gap:14px}} | |
| 165 | +.card{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 166 | + overflow:hidden;box-shadow:var(--shadow-off-soft);transition:transform .18s ease,box-shadow .18s ease; | |
| 167 | + display:flex;flex-direction:column} | |
| 168 | +.card:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)} | |
| 169 | +.card-photo{position:relative;aspect-ratio:1/1;background: | |
| 170 | + repeating-linear-gradient(-45deg,var(--green-deep) 0 14px,#33195f 14px 28px); | |
| 171 | + display:flex;align-items:center;justify-content:center;overflow:hidden; | |
| 172 | + border-bottom:1.5px solid var(--ink)} | |
| 173 | +.card-photo .init{color:var(--lime);font-family:var(--font-display);font-weight:700; | |
| 174 | + font-size:46px;letter-spacing:.02em} | |
| 175 | +.card-photo img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} | |
| 176 | +.card-photo .tier{position:absolute;top:10px;left:10px;z-index:2} | |
| 177 | +.card-photo .nplat{position:absolute;top:10px;right:10px;z-index:2;background:var(--ink); | |
| 178 | + color:var(--lime);border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 179 | + padding:2px 8px;font-family:var(--font-mono);font-size:9.5px;font-weight:700; | |
| 180 | + text-transform:uppercase;letter-spacing:.05em} | |
| 181 | +.tier{display:inline-flex;padding:2px 9px;border:1.5px solid var(--ink); | |
| 182 | + border-radius:var(--r-ctl);font-family:var(--font-mono);font-size:9.5px; | |
| 183 | + font-weight:700;text-transform:uppercase;letter-spacing:.06em;background:var(--surface);color:var(--ink)} | |
| 184 | +.tier.mega{background:var(--lime)} | |
| 185 | +.tier.macro{background:var(--amber-soft)} | |
| 186 | +.card-body{padding:14px 15px 15px;display:flex;flex-direction:column;gap:9px;flex:1} | |
| 187 | +.card-name{font-family:var(--font-display);font-size:16px;font-weight:700; | |
| 188 | + letter-spacing:-.01em;line-height:1.25;display:flex;align-items:center;gap:6px} | |
| 189 | +.vbadge{color:var(--green);flex:none;width:15px;height:15px} | |
| 190 | +.card-sub{font-family:var(--font-mono);font-size:10px;color:var(--ink-3); | |
| 191 | + text-transform:uppercase;letter-spacing:.05em;display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:-4px} | |
| 192 | +.chips{display:flex;gap:6px;flex-wrap:wrap} | |
| 193 | +.chip{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-ctl); | |
| 194 | + padding:2px 10px;font-size:11px;font-weight:600;color:var(--ink-2);white-space:nowrap} | |
| 195 | +.reach{font-size:12px;color:var(--ink-2)} | |
| 196 | +.reach b{font-family:var(--font-display);color:var(--ink);font-size:15.5px;font-weight:700} | |
| 197 | +.platrow{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;padding-top:2px} | |
| 198 | +.plat{width:32px;height:32px;border-radius:8px;border:1.5px solid var(--ink); | |
| 199 | + display:flex;align-items:center;justify-content:center;color:#fff;transition:.15s; | |
| 200 | + box-shadow:2px 2px 0 rgba(20,24,20,.25)} | |
| 201 | +.plat svg{width:15px;height:15px;fill:currentColor} | |
| 202 | +.plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)} | |
| 203 | +.more-wrap{display:flex;justify-content:center;padding:16px 0 44px} | |
| 204 | +.empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1} | |
| 205 | +.count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase; | |
| 206 | + letter-spacing:.08em;color:var(--ink-3);padding-top:18px} | |
| 207 | +.count-line b{color:var(--ink)} | |
| 208 | + | |
| 209 | +/* ===== page créateur ===== */ | |
| 210 | +.cpage{padding:30px 0 60px} | |
| 211 | +@media(max-width:640px){.cpage{padding:20px 0 40px}} | |
| 212 | +.backlink{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono); | |
| 213 | + font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em; | |
| 214 | + color:var(--green);margin-bottom:20px} | |
| 215 | +.cpage-head{display:flex;gap:26px;align-items:center;background:var(--ink); | |
| 216 | + border:1.5px solid var(--ink);border-radius:var(--r-card);padding:32px; | |
| 217 | + color:var(--paper);box-shadow:var(--shadow-off);flex-wrap:wrap} | |
| 218 | +@media(max-width:640px){.cpage-head{padding:22px 18px;gap:16px}} | |
| 219 | +.cavatar{width:104px;height:104px;border-radius:var(--r-card); | |
| 220 | + background:repeating-linear-gradient(-45deg,var(--green-deep) 0 14px,#33195f 14px 28px); | |
| 221 | + color:var(--lime);flex:none;display:flex;align-items:center;justify-content:center; | |
| 222 | + font-family:var(--font-display);font-weight:700;font-size:36px;overflow:hidden; | |
| 223 | + position:relative;border:2px solid var(--lime)} | |
| 224 | +@media(max-width:640px){.cavatar{width:76px;height:76px;font-size:26px}} | |
| 225 | +.cavatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} | |
| 226 | +.cpage-head h1{font-size:clamp(24px,4vw,40px);font-weight:700;color:var(--paper); | |
| 227 | + line-height:1.1;display:flex;align-items:center;gap:10px;flex-wrap:wrap;text-transform:uppercase} | |
| 228 | +.cpage-head h1 .vbadge{width:22px;height:22px;color:var(--lime)} | |
| 229 | +.cpage-head .meta{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px} | |
| 230 | +.mchip{background:rgba(245,243,238,.12);border:1px solid rgba(245,243,238,.3); | |
| 231 | + border-radius:var(--r-ctl);padding:3px 11px;font-family:var(--font-mono); | |
| 232 | + font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--paper)} | |
| 233 | +.mchip.o{background:var(--lime);border-color:var(--lime);color:var(--ink)} | |
| 234 | +.cpage-reach{margin-left:auto;text-align:right} | |
| 235 | +.cpage-reach b{display:block;font-family:var(--font-display); | |
| 236 | + font-size:clamp(28px,3.6vw,40px);font-weight:700;color:var(--lime);letter-spacing:-.02em} | |
| 237 | +.cpage-reach span{font-family:var(--font-mono);font-size:10px;color:rgba(245,243,238,.6); | |
| 238 | + text-transform:uppercase;letter-spacing:.08em;font-weight:700} | |
| 239 | +@media(max-width:760px){.cpage-reach{margin-left:0;text-align:left;flex-basis:100%}} | |
| 240 | +.cpage-bio{margin:24px 0 0;max-width:820px;color:var(--ink-2);font-size:15px} | |
| 241 | +.cpage-notes{margin:14px 0 0;max-width:820px;color:var(--ink-2);font-size:12.5px; | |
| 242 | + background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 243 | + padding:13px 17px;box-shadow:var(--shadow-off-soft)} | |
| 244 | +.sect-title{font-size:19px;margin:36px 0 15px;display:flex;align-items:center;gap:10px;text-transform:uppercase} | |
| 245 | +.acc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px} | |
| 246 | +@media(max-width:640px){.acc-grid{grid-template-columns:1fr}} | |
| 247 | +.acc{display:flex;align-items:center;gap:13px;background:var(--surface); | |
| 248 | + border:1.5px solid var(--ink);border-radius:var(--r-card);padding:14px 16px; | |
| 249 | + box-shadow:var(--shadow-off-soft);transition:transform .16s ease,box-shadow .16s ease;min-width:0} | |
| 250 | +.acc:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)} | |
| 251 | +.acc .plat{width:44px;height:44px;border-radius:9px;flex:none} | |
| 252 | +.acc .plat svg{width:20px;height:20px} | |
| 253 | +.acc-mid{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px;overflow:hidden} | |
| 254 | +.acc-name{font-family:var(--font-display);font-weight:700;font-size:14px;display:flex;align-items:center;gap:6px} | |
| 255 | +.acc-handle{font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis; | |
| 256 | + white-space:nowrap;display:block;max-width:100%} | |
| 257 | +.acc-right{margin-left:auto;text-align:right;flex:none;padding-left:8px} | |
| 258 | +.acc-right b{display:block;font-family:var(--font-display);font-size:16.5px;color:var(--ink);font-weight:700} | |
| 259 | +.acc-right span{font-family:var(--font-mono);font-size:9px;color:var(--ink-3); | |
| 260 | + text-transform:uppercase;letter-spacing:.06em;font-weight:700} | |
| 261 | +.conf{display:inline-flex;padding:1px 7px;border-radius:var(--r-ctl); | |
| 262 | + font-family:var(--font-mono);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em} | |
| 263 | +.conf.hi{background:var(--lime-soft);color:var(--green-deep)} | |
| 264 | +.conf.mid{background:var(--amber-soft);color:#8a5a12} | |
| 265 | +.linkinbio{display:inline-flex;align-items:center;gap:10px;margin-top:22px; | |
| 266 | + background:var(--lime);color:var(--ink);border:1.5px solid var(--ink); | |
| 267 | + border-radius:var(--r-ctl);padding:11px 20px;font-family:var(--font-display); | |
| 268 | + font-weight:700;font-size:13.5px;transition:.15s;box-shadow:4px 4px 0 rgba(20,24,20,.25)} | |
| 269 | +.linkinbio:hover{background:var(--lime-soft)} | |
| 270 | +.linkinbio:active{transform:translate(2px,2px);box-shadow:none} | |
| 271 | +.linkinbio svg{width:16px;height:16px;fill:currentColor} | |
| 272 | +.cpage-foot{margin-top:38px;font-size:12px;color:var(--ink-3); | |
| 273 | + border-top:1.5px solid var(--ink);padding-top:16px} | |
| 274 | +.cpage-foot a{color:var(--green);font-weight:600} | |
| 275 | + | |
| 276 | +/* ===== page stats ===== */ | |
| 277 | +.stats-page{padding:40px 0 70px} | |
| 278 | +.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin:26px 0} | |
| 279 | +.tile{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 280 | + padding:18px 20px;box-shadow:var(--shadow-off-soft)} | |
| 281 | +.tile-v{font-family:var(--font-display);font-weight:700;font-size:clamp(23px,3vw,32px); | |
| 282 | + letter-spacing:-.03em;line-height:1.05;color:var(--ink)} | |
| 283 | +.tile-k{font-family:var(--font-mono);font-size:9.5px;font-weight:700;text-transform:uppercase; | |
| 284 | + letter-spacing:.1em;color:var(--ink-3);margin-top:7px} | |
| 285 | +.tile.hero-tile{background:var(--ink)} | |
| 286 | +.tile.hero-tile .tile-v{color:var(--lime)} | |
| 287 | +.tile.hero-tile .tile-k{color:rgba(245,243,238,.55)} | |
| 288 | +.viz-card{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 289 | + box-shadow:var(--shadow-off-soft);padding:24px 26px 18px;margin-bottom:22px} | |
| 290 | +.viz-card h2{font-size:18px;text-transform:uppercase} | |
| 291 | +.viz-sub{font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.08em; | |
| 292 | + text-transform:uppercase;color:var(--ink-3);margin:4px 0 18px} | |
| 293 | +.hrow{display:flex;align-items:center;gap:12px;margin-bottom:10px} | |
| 294 | +.hrow .plat{width:28px;height:28px;border-radius:7px;flex:none} | |
| 295 | +.hrow .plat svg{width:13px;height:13px} | |
| 296 | +.hrow-label{flex:0 0 130px;font-size:12.5px;font-weight:600;color:var(--ink-2); | |
| 297 | + overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| 298 | +@media(max-width:640px){.hrow-label{flex-basis:96px;font-size:11.5px}} | |
| 299 | +.hbar{flex:1;height:12px;border:1.5px solid var(--ink);border-radius:4px; | |
| 300 | + background:var(--surface-2);overflow:hidden} | |
| 301 | +.hbar i{display:block;height:100%;background:var(--lime);border-right:1.5px solid var(--ink)} | |
| 302 | +.hrow .n{flex:none;font-family:var(--font-display);font-size:12.5px;font-weight:700; | |
| 303 | + color:var(--ink);min-width:44px;text-align:right} | |
| 304 | + | |
| 305 | +/* ===== retrait ===== */ | |
| 306 | +.optout-page{max-width:620px;margin:0 auto;padding:44px 0 70px} | |
| 307 | +.optout-page h1{font-size:clamp(26px,4vw,32px);margin:12px 0;text-transform:uppercase} | |
| 308 | +.optout-page p{color:var(--ink-2)} | |
| 309 | +.field{margin-top:16px} | |
| 310 | +.field label{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700; | |
| 311 | + text-transform:uppercase;letter-spacing:.08em;margin-bottom:7px;color:var(--ink-2)} | |
| 312 | +.field input,.field textarea{width:100%;border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 313 | + padding:11px 14px;font:inherit;background:var(--surface);color:var(--ink); | |
| 314 | + box-shadow:3px 3px 0 rgba(20,24,20,.08);transition:box-shadow .15s ease} | |
| 315 | +.field input:focus,.field textarea:focus{outline:2px solid var(--green);outline-offset:1px; | |
| 316 | + box-shadow:3px 3px 0 rgba(91,52,168,.25)} | |
| 317 | +@media(max-width:900px){.field input,.field textarea{font-size:16px}} | |
| 318 | +.optout-ok{background:var(--lime-soft);color:var(--green-deep);border:1.5px solid var(--ink); | |
| 319 | + border-radius:var(--r-card);padding:16px 20px;font-weight:600;margin-top:20px; | |
| 320 | + box-shadow:var(--shadow-off-soft)} | |
| 321 | +.optout-ok.warn{background:var(--amber-soft);color:#8a5a12} | |
| 322 | + | |
| 323 | +/* ===== footer + tab bar mobile (patron Lou-Ka) ===== */ | |
| 324 | +.footer{border-top:1.5px solid var(--ink);margin-top:40px;padding:32px 0 44px;background:var(--ink);color:var(--paper)} | |
| 325 | +.footer-inner{display:flex;gap:26px;flex-wrap:wrap;align-items:flex-start;justify-content:space-between} | |
| 326 | +.footer .brand{font-size:19px;color:var(--paper)} | |
| 327 | +.footer .brand .ka{color:var(--ink)} | |
| 328 | +.footer p{font-size:12px;color:rgba(245,243,238,.55);max-width:460px;margin:10px 0 0} | |
| 329 | +.footer-links{display:flex;flex-direction:column;gap:9px;font-family:var(--font-mono); | |
| 330 | + font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em; | |
| 331 | + color:rgba(245,243,238,.8)} | |
| 332 | +.footer-links a:hover{color:var(--lime)} | |
| 333 | +.fam-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px} | |
| 334 | +.fam{border:1px solid rgba(245,243,238,.3);border-radius:var(--r-ctl);padding:3px 10px; | |
| 335 | + font-family:var(--font-mono);font-size:10px;font-weight:700;color:rgba(245,243,238,.7)} | |
| 336 | +.fam:hover{background:var(--lime);color:var(--ink);border-color:var(--lime)} | |
| 337 | +.tabbar{display:none} | |
| 338 | +@media(max-width:760px){ | |
| 339 | + .tabbar{display:grid;grid-template-columns:repeat(4,1fr); | |
| 340 | + position:fixed;left:0;right:0;bottom:0;z-index:85; | |
| 341 | + background:rgba(245,243,238,.94); | |
| 342 | + backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px); | |
| 343 | + border-top:1.5px solid var(--ink); | |
| 344 | + padding:6px 4px calc(6px + env(safe-area-inset-bottom))} | |
| 345 | + .tabbar a{display:flex;flex-direction:column;align-items:center;gap:3px; | |
| 346 | + padding:5px 2px;min-height:48px;justify-content:center; | |
| 347 | + font-family:var(--font-mono);font-size:9.5px;font-weight:700; | |
| 348 | + text-transform:uppercase;letter-spacing:.04em;color:var(--ink-3); | |
| 349 | + border-radius:var(--r-ctl);transition:color .12s ease} | |
| 350 | + .tabbar a.active{color:var(--green)} | |
| 351 | + .tabbar a svg{display:block;width:20px;height:20px;fill:none;stroke:currentColor; | |
| 352 | + stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round} | |
| 353 | + body{padding-bottom:calc(64px + env(safe-area-inset-bottom))} | |
| 354 | + .footer{padding-bottom:calc(76px + env(safe-area-inset-bottom))} | |
| 355 | +} | |
| 356 | +.spin{width:26px;height:26px;border:3px solid var(--line);border-top-color:var(--green); | |
| 357 | + border-radius:50%;margin:50px auto;animation:sp .8s linear infinite} | |
| 358 | +@keyframes sp{to{transform:rotate(360deg)}} | |
| 359 | +</style> | |
| 360 | +</head> | |
| 361 | +<body> | |
| 362 | +<div id="app"></div> | |
| 363 | +<script> | |
| 364 | +"use strict"; | |
| 365 | +/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /retrait */ | |
| 366 | +const ICONS={"_author":"Simon-Pierre Boucher <contact@spboucher.ai>","_file":"frontend/src/icons.json","_desc":"Trac\u00e9s SVG des marques de plateformes (simple-icons v13, CC0) + glyphes maison (linkedin/site-web/autre)","instagram":"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077","tiktok":"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z","youtube":"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z","twitch":"M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z","kick":"M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z","x":"M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z","facebook":"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z","snapchat":"M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z","substack":"M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z","patreon":"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z","onlyfans":"M24 4.003h-4.015c-3.45 0-5.3.197-6.748 1.957a7.996 7.996 0 1 0 2.103 9.211c3.182-.231 5.39-2.134 6.085-5.173 0 0-2.399.585-4.43 0 4.018-.777 6.333-3.037 7.005-5.995zM5.61 11.999A2.391 2.391 0 0 1 9.28 9.97a2.966 2.966 0 0 1 2.998-2.528h.008c-.92 1.778-1.407 3.352-1.998 5.263A2.392 2.392 0 0 1 5.61 12Zm2.386-7.996a7.996 7.996 0 1 0 7.996 7.996 7.996 7.996 0 0 0-7.996-7.996Zm0 10.394A2.399 2.399 0 1 1 10.395 12a2.396 2.396 0 0 1-2.399 2.398Z","threads":"M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z","podcast":"M5.34 0A5.328 5.328 0 000 5.34v13.32A5.328 5.328 0 005.34 24h13.32A5.328 5.328 0 0024 18.66V5.34A5.328 5.328 0 0018.66 0zm6.525 2.568c2.336 0 4.448.902 6.056 2.587 1.224 1.272 1.912 2.619 2.264 4.392.12.59.12 2.2.007 2.864a8.506 8.506 0 01-3.24 5.296c-.608.46-2.096 1.261-2.336 1.261-.088 0-.096-.091-.056-.46.072-.592.144-.715.48-.856.536-.224 1.448-.874 2.008-1.435a7.644 7.644 0 002.008-3.536c.208-.824.184-2.656-.048-3.504-.728-2.696-2.928-4.792-5.624-5.352-.784-.16-2.208-.16-3 0-2.728.56-4.984 2.76-5.672 5.528-.184.752-.184 2.584 0 3.336.456 1.832 1.64 3.512 3.192 4.512.304.2.672.408.824.472.336.144.408.264.472.856.04.36.03.464-.056.464-.056 0-.464-.176-.896-.384l-.04-.03c-2.472-1.216-4.056-3.274-4.632-6.012-.144-.706-.168-2.392-.03-3.04.36-1.74 1.048-3.1 2.192-4.304 1.648-1.737 3.768-2.656 6.128-2.656zm.134 2.81c.409.004.803.04 1.106.106 2.784.62 4.76 3.408 4.376 6.174-.152 1.114-.536 2.03-1.216 2.88-.336.43-1.152 1.15-1.296 1.15-.023 0-.048-.272-.048-.603v-.605l.416-.496c1.568-1.878 1.456-4.502-.256-6.224-.664-.67-1.432-1.064-2.424-1.246-.64-.118-.776-.118-1.448-.008-1.02.167-1.81.562-2.512 1.256-1.72 1.704-1.832 4.342-.264 6.222l.413.496v.608c0 .336-.027.608-.06.608-.03 0-.264-.16-.512-.36l-.034-.011c-.832-.664-1.568-1.842-1.872-2.997-.184-.698-.184-2.024.008-2.72.504-1.878 1.888-3.335 3.808-4.019.41-.145 1.133-.22 1.814-.211zm-.13 2.99c.31 0 .62.06.844.178.488.253.888.745 1.04 1.259.464 1.578-1.208 2.96-2.72 2.254h-.015c-.712-.331-1.096-.956-1.104-1.77 0-.733.408-1.371 1.112-1.745.224-.117.534-.176.844-.176zm-.011 4.728c.988-.004 1.706.349 1.97.97.198.464.124 1.932-.218 4.302-.232 1.656-.36 2.074-.68 2.356-.44.39-1.064.498-1.656.288h-.003c-.716-.257-.87-.605-1.164-2.644-.341-2.37-.416-3.838-.218-4.302.262-.616.974-.966 1.97-.97z","linktree":"m13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z","linkedin":"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z","site-web":"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm7.938 9h-3.243a15.6 15.6 0 00-1.4-4.653A8.03 8.03 0 0119.938 9zM12 2.05c.9 1.2 1.9 3.2 2.5 6.95h-5C10.1 5.25 11.1 3.25 12 2.05zM4.062 15A8.06 8.06 0 013.75 12c0-1.05.15-2.05.312-3h3.55A25 25 0 007.5 12c0 1.05.05 2.05.113 3zm.643 2h3.243c.35 1.8.85 3.35 1.4 4.653A8.03 8.03 0 014.705 17zm3.243-10H4.705a8.03 8.03 0 014.643-4.653A15.6 15.6 0 007.948 7zM12 21.95c-.9-1.2-1.9-3.2-2.5-6.95h5c-.6 3.75-1.6 5.75-2.5 6.95zM14.787 13H9.213A23 23 0 019.1 12c0-1.05.037-2.05.113-3h5.574c.076.95.113 1.95.113 3s-.037 2.05-.113 3zm.508 8.653c.55-1.303 1.05-2.853 1.4-4.653h3.243a8.03 8.03 0 01-4.643 4.653zM16.388 15c.062-.95.112-1.95.112-3s-.05-2.05-.112-3h3.55c.162.95.312 1.95.312 3s-.15 2.05-.312 3z","autre":"M10.59 13.41a1 1 0 010-1.41l2.83-2.83a3 3 0 114.24 4.24l-1.41 1.42a1 1 0 11-1.42-1.42l1.42-1.41a1 1 0 10-1.42-1.42L12 13.41a1 1 0 01-1.41 0zm2.82-2.82a1 1 0 010 1.41l-2.82 2.83a3 3 0 11-4.25-4.24l1.42-1.42a1 1 0 111.41 1.42L7.76 12a1 1 0 101.41 1.41L12 10.59a1 1 0 011.41 0z"}; | |
| 367 | +const $=s=>document.querySelector(s); | |
| 368 | +const app=$("#app"); | |
| 369 | +const fmt=new Intl.NumberFormat("fr-CA",{notation:"compact",maximumFractionDigits:1}); | |
| 370 | +const fmtFull=new Intl.NumberFormat("fr-CA"); | |
| 371 | +const PLAT={ | |
| 372 | + instagram:{label:"Instagram",color:"#c13584",av:"instagram"}, | |
| 373 | + tiktok:{label:"TikTok",color:"#141814",av:"tiktok"}, | |
| 374 | + youtube:{label:"YouTube",color:"#e02424",av:"youtube"}, | |
| 375 | + twitch:{label:"Twitch",color:"#9146ff",av:"twitch"}, | |
| 376 | + kick:{label:"Kick",color:"#141814"}, | |
| 377 | + x:{label:"X (Twitter)",color:"#141814",av:"twitter"}, | |
| 378 | + facebook:{label:"Facebook",color:"#1877f2"}, | |
| 379 | + snapchat:{label:"Snapchat",color:"#d4a900"}, | |
| 380 | + substack:{label:"Substack",color:"#ff6719"}, | |
| 381 | + patreon:{label:"Patreon",color:"#f1465a"}, | |
| 382 | + onlyfans:{label:"OnlyFans",color:"#00aff0"}, | |
| 383 | + linkedin:{label:"LinkedIn",color:"#0a66c2"}, | |
| 384 | + threads:{label:"Threads",color:"#141814"}, | |
| 385 | + podcast:{label:"Balado",color:"#8940fa"}, | |
| 386 | + "site-web":{label:"Site web",color:"#5b34a8"}, | |
| 387 | + autre:{label:"Autre",color:"#8b928c"}}; | |
| 388 | +const NICHE_LBL={"humour":"Humour","mode":"Mode","beaute":"Beauté","lifestyle":"Lifestyle", | |
| 389 | + "famille-parentalite":"Famille","cuisine":"Cuisine","gaming":"Gaming","tech":"Tech", | |
| 390 | + "sport-fitness":"Sport & fitness","plein-air":"Plein air","voyage":"Voyage","musique":"Musique", | |
| 391 | + "arts":"Arts","danse":"Danse","education":"Éducation","finance-affaires":"Finance & affaires", | |
| 392 | + "sante-mieux-etre":"Santé & mieux-être","bouffe-resto":"Bouffe & resto", | |
| 393 | + "actualite-opinion":"Actualité & opinion","autre":"Autre"}; | |
| 394 | +const TIER_LBL={nano:"Nano",micro:"Micro",macro:"Macro",mega:"Méga"}; | |
| 395 | +const state={q:"",niche:"",region:"",langue:"",plateforme:"",tier:"",sort:"reach", | |
| 396 | + offset:0,items:[],total:0,stats:null,tax:null}; | |
| 397 | + | |
| 398 | +const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); | |
| 399 | +const initials=n=>n.split(/\s+/).slice(0,2).map(w=>w[0]||"").join("").toUpperCase(); | |
| 400 | +async function api(p){const r=await fetch(p);if(!r.ok)throw new Error(r.status);return r.json()} | |
| 401 | +function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`} | |
| 402 | +const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`; | |
| 403 | + | |
| 404 | +/* cascade de photos : avatar capté à la source, puis unavatar par plateforme, | |
| 405 | + repli final = initiales sur motif encre/violet (déjà en dessous) */ | |
| 406 | +function avatarSources(c){ | |
| 407 | + const order=["instagram","tiktok","youtube","twitch","x"]; | |
| 408 | + const srcs=[]; | |
| 409 | + if(c.avatar_url)srcs.push(c.avatar_url); | |
| 410 | + for(const p of order){ | |
| 411 | + const acc=(c.platforms||[]).find(a=>a.platform===p&&PLAT[p]&&PLAT[p].av); | |
| 412 | + if(acc)srcs.push(`https://unavatar.io/${PLAT[p].av}/${encodeURIComponent(acc.handle)}?fallback=false`); | |
| 413 | + } | |
| 414 | + return srcs; | |
| 415 | +} | |
| 416 | +window.nextSrc=el=>{ | |
| 417 | + const srcs=JSON.parse(el.dataset.srcs||"[]");const i=+(el.dataset.i||0)+1; | |
| 418 | + if(i<srcs.length){el.dataset.i=i;el.src=srcs[i]}else el.remove(); | |
| 419 | +}; | |
| 420 | +function photoImg(c){ | |
| 421 | + const srcs=avatarSources(c); | |
| 422 | + if(!srcs.length)return""; | |
| 423 | + return `<img loading="lazy" alt="${esc(c.display_name)}" src="${srcs[0]}" | |
| 424 | + data-srcs='${esc(JSON.stringify(srcs))}' data-i="0" onerror="nextSrc(this)">`; | |
| 425 | +} | |
| 426 | +const brandSvg=`<svg width="30" height="30" viewBox="0 0 64 64"><rect width="64" height="64" rx="10" fill="#141814"/><circle cx="32" cy="23" r="9" fill="#c9a3ff"/><path d="M14 52c3-10 9-15 18-15s15 5 18 15" stroke="#c9a3ff" stroke-width="6" fill="none" stroke-linecap="round"/></svg>`; | |
| 427 | + | |
| 428 | +function header(){ | |
| 429 | + return `<header class="header"><div class="container header-inner"> | |
| 430 | + <a href="/" data-nav class="brand">${brandSvg}Créa<span class="ka">Ka</span></a> | |
| 431 | + <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span> | |
| 432 | + <span class="header-spacer"></span> | |
| 433 | + <a class="hbtn" href="/stats" data-nav>Statistiques</a> | |
| 434 | + <a class="hbtn" href="/retrait" data-nav>Retrait</a> | |
| 435 | + <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a> | |
| 436 | + </div></header>`; | |
| 437 | +} | |
| 438 | +function tabbar(active){ | |
| 439 | + const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`; | |
| 440 | + return `<nav class="tabbar"> | |
| 441 | + ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")} | |
| 442 | + ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")} | |
| 443 | + ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")} | |
| 444 | + ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")} | |
| 445 | + </nav>`; | |
| 446 | +} | |
| 447 | +function footer(){ | |
| 448 | + return `<footer class="footer"><div class="container footer-inner"> | |
| 449 | + <div><span class="brand">${brandSvg}Créa<span class="ka" style="background:var(--lime)">Ka</span></span> | |
| 450 | + <p>Annuaire public des créateurs de contenu québécois. Uniquement des profils publics | |
| 451 | + et professionnels ; aucune donnée privée. Retrait simple et respecté (Loi 25). | |
| 452 | + — Simon-Pierre Boucher</p> | |
| 453 | + <div class="fam-row"><span class="klabel" style="color:rgba(245,243,238,.5);align-self:center">Groupe KA :</span> | |
| 454 | + <a class="fam" href="https://www.groupe-ka.com" target="_blank" rel="noopener">KA ID</a> | |
| 455 | + <a class="fam" href="https://www.lou-ka.com" target="_blank" rel="noopener">Lou-Ka</a> | |
| 456 | + <a class="fam" href="https://www.immo-ka.com" target="_blank" rel="noopener">Immo-Ka</a> | |
| 457 | + <a class="fam" href="https://www.auto-ka.com" target="_blank" rel="noopener">Auto-Ka</a> | |
| 458 | + <a class="fam" href="https://www.food-ka.com" target="_blank" rel="noopener">Food-Ka</a> | |
| 459 | + <a class="fam" href="https://www.fabri-ka.com" target="_blank" rel="noopener">Fabri-Ka</a></div></div> | |
| 460 | + <div class="footer-links"> | |
| 461 | + <a href="/stats" data-nav>Statistiques</a> | |
| 462 | + <a href="/retrait" data-nav>Demander un retrait</a> | |
| 463 | + <a href="/api/stats" target="_blank" rel="noopener">API publique</a> | |
| 464 | + </div> | |
| 465 | + </div></footer>`; | |
| 466 | +} | |
| 467 | + | |
| 468 | +/* ---------- annuaire ---------- */ | |
| 469 | +function platIcon(a){ | |
| 470 | + const p=PLAT[a.platform]||PLAT.autre; | |
| 471 | + return `<a class="plat" href="${esc(a.url)}" target="_blank" rel="noopener" | |
| 472 | + title="${p.label} — @${esc(a.handle)}" style="background:${p.color}" | |
| 473 | + onclick="event.stopPropagation()">${icon(a.platform)}</a>`; | |
| 474 | +} | |
| 475 | +function cardHtml(c){ | |
| 476 | + const plats=(c.platforms||[]).slice(0,7).map(platIcon).join(""); | |
| 477 | + const niches=(c.niches||[]).slice(0,2).map(n=>`<span class="chip">${NICHE_LBL[n]||n}</span>`).join(""); | |
| 478 | + const verified=(c.platforms||[]).some(a=>a.verified); | |
| 479 | + const np=(c.platforms||[]).length; | |
| 480 | + return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer"> | |
| 481 | + <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)} | |
| 482 | + <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> | |
| 483 | + <span class="nplat">${np} plat.</span></div> | |
| 484 | + <div class="card-body"> | |
| 485 | + <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div> | |
| 486 | + <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div> | |
| 487 | + <div class="chips">${niches}</div> | |
| 488 | + ${c.total_reach?`<div class="reach"><b>${fmt.format(c.total_reach)}</b> abonnés cumulés</div>`:""} | |
| 489 | + <div class="platrow">${plats}</div></div></div>`; | |
| 490 | +} | |
| 491 | +function selectHtml(id,label,opts,val,lbls){ | |
| 492 | + const o=opts.map(v=>`<option value="${esc(v)}"${v===val?" selected":""}>${esc(lbls?(lbls[v]||v):v)}</option>`).join(""); | |
| 493 | + return `<select class="f${val?" active":""}" id="${id}"><option value="">${label}</option>${o}</select>`; | |
| 494 | +} | |
| 495 | +async function renderHome(){ | |
| 496 | + document.title="Créa-Ka — Tous les créateurs québécois. Tous leurs liens."; | |
| 497 | + if(!state.tax)state.tax=await api("/api/taxonomies"); | |
| 498 | + if(!state.stats)state.stats=await api("/api/stats").catch(()=>null); | |
| 499 | + const s=state.stats||{creators:0,accounts:0,by_platform:{}}; | |
| 500 | + app.innerHTML=header()+` | |
| 501 | + <section class="hero"><div class="container"> | |
| 502 | + <span class="kicker">Annuaire des créateurs du Québec</span> | |
| 503 | + <h1 style="margin-top:16px">Chaque créateur<br>québécois. <span class="outline-txt">Tous</span> ses <span class="hl">liens.</span></h1> | |
| 504 | + <p class="lead">Un « Linktree collectif » du Québec : influenceurs, youtubeurs, streamers, | |
| 505 | + podcasteurs — leurs comptes regroupés et découvrables par niche, région, langue et taille d'audience.</p> | |
| 506 | + <div class="stats-row"> | |
| 507 | + <span class="stat-pill"><b>${fmtFull.format(s.creators)}</b> créateurs</span> | |
| 508 | + <span class="stat-pill"><b>${fmtFull.format(s.accounts)}</b> comptes reliés</span> | |
| 509 | + <span class="stat-pill"><b>${Object.keys(s.by_platform||{}).length}</b> plateformes</span> | |
| 510 | + </div> | |
| 511 | + <div class="advantages"> | |
| 512 | + <div class="adv"><div class="ico">${icon("autre")}</div><h3>Cross-plateforme</h3><p>Instagram, TikTok, YouTube, Twitch, X… tous les comptes d'une même personne, au même endroit.</p></div> | |
| 513 | + <div class="adv"><div class="ico"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5" stroke-linecap="round"/></svg></div><h3>Découvrable</h3><p>Filtres par niche, région, langue, plateforme et taille d'audience.</p></div> | |
| 514 | + <div class="adv"><div class="ico">✓</div><h3>Liens vérifiés</h3><p>Chaque rattachement porte un score de confiance ; mieux vaut un lien de moins qu'un lien erroné.</p></div> | |
| 515 | + <div class="adv"><div class="ico">🛡</div><h3>Respectueux</h3><p>Profils publics seulement, retrait (opt-out) simple et respecté. Loi 25.</p></div> | |
| 516 | + </div> | |
| 517 | + </div></section> | |
| 518 | + <div class="searchbar" id="annuaire"><div class="container search-wrap"> | |
| 519 | + <label class="search-input"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/><path d="M20 20l-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> | |
| 520 | + <input id="q" placeholder="Chercher un créateur…" value="${esc(state.q)}"></label> | |
| 521 | + <div class="filters"> | |
| 522 | + ${selectHtml("f-niche","Niche",state.tax.niches,state.niche,NICHE_LBL)} | |
| 523 | + ${selectHtml("f-region","Région",state.tax.regions,state.region)} | |
| 524 | + ${selectHtml("f-plat","Plateforme",state.tax.plateformes,state.plateforme,Object.fromEntries(Object.entries(PLAT).map(([k,v])=>[k,v.label])))} | |
| 525 | + ${selectHtml("f-tier","Taille",state.tax.tiers,state.tier,TIER_LBL)} | |
| 526 | + ${selectHtml("f-langue","Langue",state.tax.langues,state.langue,{fr:"Français",en:"Anglais",bilingue:"Bilingue"})} | |
| 527 | + ${selectHtml("f-sort","Tri",["reach","nom","recent"],state.sort==="reach"?"":state.sort,{reach:"Audience",nom:"Nom A-Z",recent:"Récents"})} | |
| 528 | + </div> | |
| 529 | + </div></div> | |
| 530 | + <main class="container"><div class="count-line" id="countline"></div> | |
| 531 | + <div class="grid" id="grid"><div class="spin"></div></div> | |
| 532 | + <div class="more-wrap" id="more"></div></main>`+footer()+tabbar(location.hash==="#annuaire"?"search":"home"); | |
| 533 | + bindNav(); | |
| 534 | + $("#q").addEventListener("input",deb(()=>{state.q=$("#q").value;reload()},300)); | |
| 535 | + [["f-niche","niche"],["f-region","region"],["f-plat","plateforme"], | |
| 536 | + ["f-tier","tier"],["f-langue","langue"],["f-sort","sort"]].forEach(([id,key])=>{ | |
| 537 | + $("#"+id).addEventListener("change",e=>{state[key]=e.target.value||(key==="sort"?"reach":""); | |
| 538 | + e.target.classList.toggle("active",!!e.target.value);reload()})}); | |
| 539 | + if(location.hash==="#annuaire"){const el=$("#annuaire");if(el)el.scrollIntoView()} | |
| 540 | + await reload(); | |
| 541 | +} | |
| 542 | +function deb(fn,ms){let t;return()=>{clearTimeout(t);t=setTimeout(fn,ms)}} | |
| 543 | +async function reload(more){ | |
| 544 | + if(!more){state.offset=0;state.items=[]} | |
| 545 | + const p=new URLSearchParams({q:state.q,niche:state.niche,region:state.region, | |
| 546 | + langue:state.langue,plateforme:state.plateforme,tier:state.tier,sort:state.sort, | |
| 547 | + limit:"60",offset:String(state.offset)}); | |
| 548 | + const data=await api("/api/creators?"+p).catch(()=>({items:[],total:0})); | |
| 549 | + state.items=state.items.concat(data.items);state.total=data.total; | |
| 550 | + const grid=$("#grid");if(!grid)return; | |
| 551 | + $("#countline").innerHTML=`<b>${fmtFull.format(state.total)}</b> créateur${state.total>1?"s":""} trouvé${state.total>1?"s":""}`; | |
| 552 | + grid.innerHTML=state.items.length?state.items.map(cardHtml).join(""): | |
| 553 | + `<div class="empty">Aucun créateur ne correspond à ces filtres.</div>`; | |
| 554 | + const moreDiv=$("#more"); | |
| 555 | + moreDiv.innerHTML=state.items.length<state.total? | |
| 556 | + `<button class="btn btn-primary" id="loadmore">Afficher plus (${fmtFull.format(state.total-state.items.length)} restants)</button>`:""; | |
| 557 | + const lm=$("#loadmore");if(lm)lm.onclick=()=>{state.offset+=60;reload(true)}; | |
| 558 | + bindNav(grid); | |
| 559 | +} | |
| 560 | + | |
| 561 | +/* ---------- page créateur ---------- */ | |
| 562 | +async function renderCreator(id){ | |
| 563 | + app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("");bindNav(); | |
| 564 | + let c;try{c=await api("/api/creators/"+encodeURIComponent(id))} | |
| 565 | + catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2> | |
| 566 | + <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return} | |
| 567 | + document.title=`${c.display_name} — Créa-Ka`; | |
| 568 | + const verified=(c.platforms||[]).some(a=>a.verified); | |
| 569 | + const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join(""); | |
| 570 | + const M_LBL={following:"abonnements",posts:"publications",likes:"j'aime", | |
| 571 | + views:"vues",videos:"vidéos",episodes:"épisodes"}; | |
| 572 | + const accs=(c.platforms||[]).map(a=>{const p=PLAT[a.platform]||PLAT.autre; | |
| 573 | + const conf=(a.confidence||0)>=0.75?["hi","confiance élevée"]:["mid","à confirmer"]; | |
| 574 | + const mx=Object.entries(a.metrics||{}).filter(([k,v])=>M_LBL[k]&&v!=null) | |
| 575 | + .map(([k,v])=>`${fmt.format(v)} ${M_LBL[k]}`).join(" · "); | |
| 576 | + return `<a class="acc" href="${esc(a.url)}" target="_blank" rel="noopener"> | |
| 577 | + <span class="plat" style="background:${p.color}">${icon(a.platform)}</span> | |
| 578 | + <span class="acc-mid"><span class="acc-name">${p.label}${a.verified?checkSvg:""}</span> | |
| 579 | + <span class="acc-handle">@${esc(a.handle)}</span> | |
| 580 | + <span class="acc-handle" style="overflow:visible"><span class="conf ${conf[0]}" title="Score de rattachement ${Math.round((a.confidence||0)*100)} %">${conf[1]}</span>${mx?` <span style="font-size:11px">${mx}</span>`:""}</span></span> | |
| 581 | + <span class="acc-right">${a.followers?`<b>${fmt.format(a.followers)}</b><span>abonnés</span>`:`<span style="font-size:11px">voir le profil ↗</span>`}</span></a>`}).join(""); | |
| 582 | + const checked=(c.platforms||[]).map(a=>a.last_checked).filter(Boolean).sort().pop(); | |
| 583 | + app.innerHTML=header()+` | |
| 584 | + <div class="container cpage"> | |
| 585 | + <a class="backlink" href="/" data-nav>← Tous les créateurs</a> | |
| 586 | + <div class="cpage-head"> | |
| 587 | + <div class="cavatar">${esc(initials(c.display_name))}${photoImg(c)}</div> | |
| 588 | + <div style="flex:1;min-width:200px"><h1>${esc(c.display_name)}${verified?checkSvg:""}</h1> | |
| 589 | + <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> | |
| 590 | + ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""} | |
| 591 | + ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div></div> | |
| 592 | + ${c.total_reach?`<div class="cpage-reach"><b>${fmt.format(c.total_reach)}</b><span>abonnés cumulés</span></div>`:""} | |
| 593 | + </div> | |
| 594 | + ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""} | |
| 595 | + ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""} | |
| 596 | + <h2 class="sect-title">Ses plateformes <span class="chip">${(c.platforms||[]).length}</span></h2> | |
| 597 | + <div class="acc-grid">${accs}</div> | |
| 598 | + ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""} | |
| 599 | + <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes. | |
| 600 | + Audiences approximatives${checked?`, dernière vérification ${esc(checked.slice(0,10))}`:""}. | |
| 601 | + Une erreur dans cette fiche ? <a href="/retrait" data-nav>Signaler / demander un retrait</a>.</div> | |
| 602 | + </div>`+footer()+tabbar(""); | |
| 603 | + bindNav();window.scrollTo(0,0); | |
| 604 | +} | |
| 605 | + | |
| 606 | +/* ---------- page stats ---------- */ | |
| 607 | +async function renderStats(){ | |
| 608 | + document.title="Statistiques — Créa-Ka"; | |
| 609 | + app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("stats");bindNav(); | |
| 610 | + const s=await api("/api/stats").catch(()=>null); | |
| 611 | + if(!s){app.innerHTML=header()+`<div class="container empty">Statistiques indisponibles.</div>`+footer()+tabbar("stats");bindNav();return} | |
| 612 | + const bars=(obj,lbls,useIcon)=>{const entries=Object.entries(obj).slice(0,12); | |
| 613 | + const max=Math.max(...entries.map(e=>e[1]),1); | |
| 614 | + return entries.map(([k,v])=>`<div class="hrow"> | |
| 615 | + ${useIcon?`<span class="plat" style="background:${(PLAT[k]||PLAT.autre).color}">${icon(k)}</span>`:""} | |
| 616 | + <span class="hrow-label">${esc(lbls?(lbls[k]||k):k)}</span> | |
| 617 | + <span class="hbar"><i style="width:${Math.max(2,Math.round(v/max*100))}%"></i></span> | |
| 618 | + <span class="n">${fmtFull.format(v)}</span></div>`).join("")}; | |
| 619 | + app.innerHTML=header()+` | |
| 620 | + <div class="container stats-page"> | |
| 621 | + <span class="kicker">Portrait de l'annuaire</span> | |
| 622 | + <h1 style="font-size:clamp(28px,4.6vw,46px);margin:12px 0 4px;text-transform:uppercase">Les créateurs québécois <span class="hl">en chiffres.</span></h1> | |
| 623 | + <div class="tiles"> | |
| 624 | + <div class="tile hero-tile"><div class="tile-v">${fmtFull.format(s.creators)}</div><div class="tile-k">Créateurs</div></div> | |
| 625 | + <div class="tile"><div class="tile-v">${fmtFull.format(s.accounts)}</div><div class="tile-k">Comptes reliés</div></div> | |
| 626 | + <div class="tile"><div class="tile-v">${Object.keys(s.by_platform||{}).length}</div><div class="tile-k">Plateformes</div></div> | |
| 627 | + <div class="tile"><div class="tile-v">${(s.accounts/Math.max(1,s.creators)).toFixed(1)}</div><div class="tile-k">Comptes / créateur</div></div> | |
| 628 | + </div> | |
| 629 | + <div class="viz-card"><h2>Par plateforme</h2><div class="viz-sub">comptes publics reliés</div> | |
| 630 | + ${bars(s.by_platform,Object.fromEntries(Object.entries(PLAT).map(([k,v])=>[k,v.label])),true)}</div> | |
| 631 | + <div class="viz-card"><h2>Par niche</h2><div class="viz-sub">créateurs actifs</div> | |
| 632 | + ${bars(s.by_niche,NICHE_LBL)}</div> | |
| 633 | + <div class="viz-card"><h2>Par taille d'audience</h2><div class="viz-sub">basée sur la plateforme principale</div> | |
| 634 | + ${bars(s.by_tier,TIER_LBL)}</div> | |
| 635 | + ${Object.keys(s.by_region||{}).length?`<div class="viz-card"><h2>Par région déclarée</h2><div class="viz-sub">quand le créateur la rend publique</div>${bars(s.by_region)}</div>`:""} | |
| 636 | + </div>`+footer()+tabbar("stats"); | |
| 637 | + bindNav();window.scrollTo(0,0); | |
| 638 | +} | |
| 639 | + | |
| 640 | +/* ---------- retrait / opt-out ---------- */ | |
| 641 | +function renderOptout(){ | |
| 642 | + document.title="Retrait (opt-out) — Créa-Ka"; | |
| 643 | + app.innerHTML=header()+` | |
| 644 | + <div class="container optout-page"> | |
| 645 | + <span class="kicker">Vie privée d'abord</span> | |
| 646 | + <h1>Demander un <span class="hl">retrait</span></h1> | |
| 647 | + <p>Vous êtes créateur ou créatrice et souhaitez ne plus figurer dans l'annuaire ? | |
| 648 | + La fiche est <b>masquée immédiatement</b> et exclue de toute ré-agrégation future. | |
| 649 | + Une correction d'erreur (mauvais compte rattaché) passe par le même formulaire.</p> | |
| 650 | + <form id="of"> | |
| 651 | + <div class="field"><label>Nom public / nom de scène</label><input id="o-name" maxlength="200" placeholder="ex. Prénom Nom"></div> | |
| 652 | + <div class="field"><label>Un de vos comptes (plateforme:handle)</label><input id="o-acc" maxlength="200" placeholder="ex. instagram:monhandle"></div> | |
| 653 | + <div class="field"><label>Raison (facultatif)</label><textarea id="o-reason" rows="3" maxlength="500"></textarea></div> | |
| 654 | + <div class="field"><label>Courriel de contact (facultatif, pour confirmation)</label><input id="o-contact" maxlength="200" type="email"></div> | |
| 655 | + <div class="field"><button class="btn btn-primary" type="submit">Masquer ma fiche</button></div> | |
| 656 | + </form><div id="o-result"></div> | |
| 657 | + </div>`+footer()+tabbar("optout"); | |
| 658 | + bindNav(); | |
| 659 | + $("#of").addEventListener("submit",async e=>{e.preventDefault(); | |
| 660 | + const body={name:$("#o-name").value.trim(),account:$("#o-acc").value.trim(), | |
| 661 | + reason:$("#o-reason").value.trim(),contact:$("#o-contact").value.trim()}; | |
| 662 | + if(!body.name&&!body.account){$("#o-result").innerHTML=`<div class="optout-ok warn">Indiquez au moins un nom ou un compte.</div>`;return} | |
| 663 | + try{const r=await fetch("/api/optout",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}); | |
| 664 | + const j=await r.json(); | |
| 665 | + $("#o-result").innerHTML=`<div class="optout-ok">✓ ${esc(j.message||"Demande enregistrée.")} ${j.masked?`(${j.masked} fiche(s) masquée(s))`:""}</div>`; | |
| 666 | + e.target.reset();} | |
| 667 | + catch(err){$("#o-result").innerHTML=`<div class="optout-ok warn">Erreur — réessayez ou écrivez à contact@spboucher.ai.</div>`}}); | |
| 668 | +} | |
| 669 | + | |
| 670 | +/* ---------- routeur ---------- */ | |
| 671 | +function bindNav(root){ | |
| 672 | + (root||document).querySelectorAll("[data-nav]").forEach(a=>{ | |
| 673 | + if(a._nav)return;a._nav=1; | |
| 674 | + a.addEventListener("click",e=>{const href=a.getAttribute("href"); | |
| 675 | + e.preventDefault();history.pushState(null,"",href);route(); | |
| 676 | + if(href.endsWith("#annuaire")){const el=$("#annuaire");if(el){el.scrollIntoView();const q=$("#q");if(q)q.focus()}}})}); | |
| 677 | + (root||document).querySelectorAll("[data-href]").forEach(card=>{ | |
| 678 | + if(card._nav)return;card._nav=1; | |
| 679 | + const go=()=>{history.pushState(null,"",card.dataset.href);route()}; | |
| 680 | + card.addEventListener("click",e=>{if(e.target.closest("a"))return;go()}); | |
| 681 | + card.addEventListener("keydown",e=>{if(e.key==="Enter")go()}); | |
| 682 | + });} | |
| 683 | +function route(){ | |
| 684 | + const p=location.pathname; | |
| 685 | + const m=p.match(/^\/createur\/([^/]+)$/); | |
| 686 | + if(m)return renderCreator(decodeURIComponent(m[1])); | |
| 687 | + if(p==="/retrait")return renderOptout(); | |
| 688 | + if(p==="/stats")return renderStats(); | |
| 689 | + return renderHome(); | |
| 690 | +} | |
| 691 | +window.addEventListener("popstate",route); | |
| 692 | +route(); | |
| 693 | +</script> | |
| 694 | +</body> | |
| 695 | +</html> | |
added
frontend/src/icons.json
+22 −0
@@ -0,0 +1,22 @@ | ||
| 1 | +{ | |
| 2 | +"_author": "Simon-Pierre Boucher <contact@spboucher.ai>", | |
| 3 | +"_file": "frontend/src/icons.json", | |
| 4 | +"_desc": "Tracés SVG des marques de plateformes (simple-icons v13, CC0) + glyphes maison (linkedin/site-web/autre)", | |
| 5 | +"instagram": "M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077", | |
| 6 | +"tiktok": "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z", | |
| 7 | +"youtube": "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z", | |
| 8 | +"twitch": "M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z", | |
| 9 | +"kick": "M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z", | |
| 10 | +"x": "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z", | |
| 11 | +"facebook": "M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z", | |
| 12 | +"snapchat": "M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z", | |
| 13 | +"substack": "M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z", | |
| 14 | +"patreon": "M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z", | |
| 15 | +"onlyfans": "M24 4.003h-4.015c-3.45 0-5.3.197-6.748 1.957a7.996 7.996 0 1 0 2.103 9.211c3.182-.231 5.39-2.134 6.085-5.173 0 0-2.399.585-4.43 0 4.018-.777 6.333-3.037 7.005-5.995zM5.61 11.999A2.391 2.391 0 0 1 9.28 9.97a2.966 2.966 0 0 1 2.998-2.528h.008c-.92 1.778-1.407 3.352-1.998 5.263A2.392 2.392 0 0 1 5.61 12Zm2.386-7.996a7.996 7.996 0 1 0 7.996 7.996 7.996 7.996 0 0 0-7.996-7.996Zm0 10.394A2.399 2.399 0 1 1 10.395 12a2.396 2.396 0 0 1-2.399 2.398Z", | |
| 16 | +"threads": "M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z", | |
| 17 | +"podcast": "M5.34 0A5.328 5.328 0 000 5.34v13.32A5.328 5.328 0 005.34 24h13.32A5.328 5.328 0 0024 18.66V5.34A5.328 5.328 0 0018.66 0zm6.525 2.568c2.336 0 4.448.902 6.056 2.587 1.224 1.272 1.912 2.619 2.264 4.392.12.59.12 2.2.007 2.864a8.506 8.506 0 01-3.24 5.296c-.608.46-2.096 1.261-2.336 1.261-.088 0-.096-.091-.056-.46.072-.592.144-.715.48-.856.536-.224 1.448-.874 2.008-1.435a7.644 7.644 0 002.008-3.536c.208-.824.184-2.656-.048-3.504-.728-2.696-2.928-4.792-5.624-5.352-.784-.16-2.208-.16-3 0-2.728.56-4.984 2.76-5.672 5.528-.184.752-.184 2.584 0 3.336.456 1.832 1.64 3.512 3.192 4.512.304.2.672.408.824.472.336.144.408.264.472.856.04.36.03.464-.056.464-.056 0-.464-.176-.896-.384l-.04-.03c-2.472-1.216-4.056-3.274-4.632-6.012-.144-.706-.168-2.392-.03-3.04.36-1.74 1.048-3.1 2.192-4.304 1.648-1.737 3.768-2.656 6.128-2.656zm.134 2.81c.409.004.803.04 1.106.106 2.784.62 4.76 3.408 4.376 6.174-.152 1.114-.536 2.03-1.216 2.88-.336.43-1.152 1.15-1.296 1.15-.023 0-.048-.272-.048-.603v-.605l.416-.496c1.568-1.878 1.456-4.502-.256-6.224-.664-.67-1.432-1.064-2.424-1.246-.64-.118-.776-.118-1.448-.008-1.02.167-1.81.562-2.512 1.256-1.72 1.704-1.832 4.342-.264 6.222l.413.496v.608c0 .336-.027.608-.06.608-.03 0-.264-.16-.512-.36l-.034-.011c-.832-.664-1.568-1.842-1.872-2.997-.184-.698-.184-2.024.008-2.72.504-1.878 1.888-3.335 3.808-4.019.41-.145 1.133-.22 1.814-.211zm-.13 2.99c.31 0 .62.06.844.178.488.253.888.745 1.04 1.259.464 1.578-1.208 2.96-2.72 2.254h-.015c-.712-.331-1.096-.956-1.104-1.77 0-.733.408-1.371 1.112-1.745.224-.117.534-.176.844-.176zm-.011 4.728c.988-.004 1.706.349 1.97.97.198.464.124 1.932-.218 4.302-.232 1.656-.36 2.074-.68 2.356-.44.39-1.064.498-1.656.288h-.003c-.716-.257-.87-.605-1.164-2.644-.341-2.37-.416-3.838-.218-4.302.262-.616.974-.966 1.97-.97z", | |
| 18 | +"linktree": "m13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z", | |
| 19 | +"linkedin": "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z", | |
| 20 | +"site-web": "M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm7.938 9h-3.243a15.6 15.6 0 00-1.4-4.653A8.03 8.03 0 0119.938 9zM12 2.05c.9 1.2 1.9 3.2 2.5 6.95h-5C10.1 5.25 11.1 3.25 12 2.05zM4.062 15A8.06 8.06 0 013.75 12c0-1.05.15-2.05.312-3h3.55A25 25 0 007.5 12c0 1.05.05 2.05.113 3zm.643 2h3.243c.35 1.8.85 3.35 1.4 4.653A8.03 8.03 0 014.705 17zm3.243-10H4.705a8.03 8.03 0 014.643-4.653A15.6 15.6 0 007.948 7zM12 21.95c-.9-1.2-1.9-3.2-2.5-6.95h5c-.6 3.75-1.6 5.75-2.5 6.95zM14.787 13H9.213A23 23 0 019.1 12c0-1.05.037-2.05.113-3h5.574c.076.95.113 1.95.113 3s-.037 2.05-.113 3zm.508 8.653c.55-1.303 1.05-2.853 1.4-4.653h3.243a8.03 8.03 0 01-4.643 4.653zM16.388 15c.062-.95.112-1.95.112-3s-.05-2.05-.112-3h3.55c.162.95.312 1.95.312 3s-.15 2.05-.312 3z", | |
| 21 | +"autre": "M10.59 13.41a1 1 0 010-1.41l2.83-2.83a3 3 0 114.24 4.24l-1.41 1.42a1 1 0 11-1.42-1.42l1.42-1.41a1 1 0 10-1.42-1.42L12 13.41a1 1 0 01-1.41 0zm2.82-2.82a1 1 0 010 1.41l-2.82 2.83a3 3 0 11-4.25-4.24l1.42-1.42a1 1 0 111.41 1.42L7.76 12a1 1 0 101.41 1.41L12 10.59a1 1 0 011.41 0z" | |
| 22 | +} | |
| \ No newline at end of file | ||
added
frontend/src/index.template.html
+695 −0
@@ -0,0 +1,695 @@ | ||
| 1 | +<!-- | |
| 2 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | + File: frontend/src/index.template.html | |
| 4 | + Desc: Gabarit du frontend Créa-Ka — __ICONS__ est remplacé par les tracés | |
| 5 | + SVG des marques (simple-icons, CC0) via scripts/build_frontend.py. | |
| 6 | + Système de design « éditorial sharp » du Groupe KA (identité partagée | |
| 7 | + avec groupe-ka.com, Vrai-Prix, ValoPlex : papier grainé #f5f3ee, encre | |
| 8 | + #141814, bordures encre + ombres décalées dures, Space Grotesk / Inter | |
| 9 | + / JetBrains Mono) — accent propre Créa-Ka : VIOLET ÉLECTRIQUE. | |
| 10 | + Patrons mobiles calqués sur Lou-Ka (tab bar 4 onglets, filtres | |
| 11 | + défilants, anti-zoom iOS). Routes : / · /createur/{id} · /stats · /retrait | |
| 12 | +--> | |
| 13 | +<!DOCTYPE html> | |
| 14 | +<html lang="fr"> | |
| 15 | +<head> | |
| 16 | +<meta charset="utf-8"> | |
| 17 | +<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | |
| 18 | +<title>Créa-Ka — Tous les créateurs québécois. Tous leurs liens.</title> | |
| 19 | +<meta name="description" content="Annuaire public des créateurs de contenu et influenceurs québécois : tous leurs comptes (Instagram, TikTok, YouTube, Twitch…) regroupés, cherchables par niche, région, langue et taille d'audience."> | |
| 20 | +<meta name="theme-color" content="#141814"> | |
| 21 | +<link rel="preconnect" href="https://fonts.googleapis.com"> | |
| 22 | +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| 23 | +<link rel="preconnect" href="https://unavatar.io"> | |
| 24 | +<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet"> | |
| 25 | +<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%23141814'/%3E%3Ccircle cx='32' cy='23' r='9' fill='%23c9a3ff'/%3E%3Cpath d='M14 52c3-10 9-15 18-15s15 5 18 15' stroke='%23c9a3ff' stroke-width='6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"> | |
| 26 | +<style> | |
| 27 | +/* système « éditorial sharp » Groupe KA — accent Créa-Ka : violet électrique. | |
| 28 | + Convention du groupe : les NOMS de variables (--green/--lime) sont partagés | |
| 29 | + entre les apps, seules les VALEURS changent. */ | |
| 30 | +:root{ | |
| 31 | + --paper:#f5f3ee; --surface:#fff; --surface-2:#faf9f5; | |
| 32 | + --ink:#141814; --ink-2:#4d5551; --ink-3:#8b928c; | |
| 33 | + --line:rgba(20,24,20,.14); --line-strong:rgba(20,24,20,.85); | |
| 34 | + --green:#5b34a8; /* violet profond (primaire) */ | |
| 35 | + --green-deep:#3d2178; | |
| 36 | + --lime:#c9a3ff; /* violet électrique (accent) */ | |
| 37 | + --lime-soft:#efe6ff; | |
| 38 | + --amber:#e8a33d; --amber-soft:#fdf3e2; | |
| 39 | + --danger:#b3423a; | |
| 40 | + --r-card:10px; --r-ctl:6px; --r-pill:999px; | |
| 41 | + --shadow-flat:0 1px 2px rgba(20,24,20,.05); | |
| 42 | + --shadow-off:6px 6px 0 var(--ink); | |
| 43 | + --shadow-off-soft:8px 8px 0 rgba(20,24,20,.08); | |
| 44 | + --font-display:"Space Grotesk",system-ui,sans-serif; | |
| 45 | + --font-body:"Inter",system-ui,sans-serif; | |
| 46 | + --font-mono:"JetBrains Mono",ui-monospace,monospace; | |
| 47 | +} | |
| 48 | +*{box-sizing:border-box} | |
| 49 | +html{scroll-behavior:smooth} | |
| 50 | +body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font-body); | |
| 51 | + font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased; | |
| 52 | + padding-bottom:env(safe-area-inset-bottom)} | |
| 53 | +/* grain de film pleine page (signature du groupe) — aucun z-index sur body>* */ | |
| 54 | +body::before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.35;z-index:9999; | |
| 55 | + background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E")} | |
| 56 | +body>*{position:relative} | |
| 57 | +h1,h2,h3,h4{font-family:var(--font-display);letter-spacing:-.03em;margin:0} | |
| 58 | +a{color:inherit;text-decoration:none} button{font-family:inherit;cursor:pointer} img{display:block} | |
| 59 | +::selection{background:var(--lime);color:var(--ink)} | |
| 60 | +:focus-visible{outline:2px solid var(--green);outline-offset:2px} | |
| 61 | +.icn{width:1em;height:1em;display:block;fill:currentColor} | |
| 62 | +.container{max-width:1240px;margin:0 auto;padding:0 24px} | |
| 63 | +@media(max-width:640px){.container{padding:0 16px}} | |
| 64 | +.kicker{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono); | |
| 65 | + font-size:11.5px;font-weight:500;text-transform:uppercase;letter-spacing:.12em;color:var(--green)} | |
| 66 | +.kicker::before{content:"";width:22px;height:2px;background:var(--green)} | |
| 67 | +.klabel{font-family:var(--font-mono);font-size:10px;font-weight:700; | |
| 68 | + text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3)} | |
| 69 | +.hl{display:inline-block;background:var(--lime);border-radius:8px;padding:0 10px 2px;transform:rotate(-1deg)} | |
| 70 | +.outline-txt{color:transparent;-webkit-text-stroke:2px var(--ink)} | |
| 71 | + | |
| 72 | +/* ---------- cartes & boutons (langage du groupe) ---------- */ | |
| 73 | +.gk-card{background:var(--surface);border:1.5px solid var(--ink); | |
| 74 | + border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)} | |
| 75 | +.gk-hover{transition:transform .18s ease,box-shadow .18s ease} | |
| 76 | +.gk-hover:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)} | |
| 77 | +.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px; | |
| 78 | + padding:10px 20px;border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 79 | + font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer; | |
| 80 | + transition:background .15s ease,transform .1s ease,box-shadow .1s ease} | |
| 81 | +.btn:active{transform:translate(2px,2px);box-shadow:none!important} | |
| 82 | +.btn-primary{background:var(--ink);color:var(--lime);box-shadow:4px 4px 0 rgba(20,24,20,.25)} | |
| 83 | +.btn-primary:hover{background:var(--green-deep)} | |
| 84 | +.btn-ghost{background:transparent;color:var(--ink)} | |
| 85 | +.btn-ghost:hover{background:var(--lime)} | |
| 86 | + | |
| 87 | +/* ===== header ===== */ | |
| 88 | +.header{position:sticky;top:0;z-index:50;background:rgba(245,243,238,.9); | |
| 89 | + backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1.5px solid var(--ink)} | |
| 90 | +.header-inner{display:flex;align-items:center;gap:16px;height:64px} | |
| 91 | +.brand{font-family:var(--font-display);font-weight:700;font-size:23px;letter-spacing:-.03em; | |
| 92 | + display:flex;align-items:center;line-height:1;color:var(--ink)} | |
| 93 | +.brand svg{margin-right:11px;flex:none} | |
| 94 | +.brand .ka{background:var(--lime);border-radius:6px;padding:0 6px 1px;margin-left:2px;transform:rotate(-1deg)} | |
| 95 | +.brand-tag{font-family:var(--font-mono);font-size:10px;font-weight:500;color:var(--ink-3); | |
| 96 | + line-height:1.5;text-transform:uppercase;letter-spacing:.08em} | |
| 97 | +@media(max-width:900px){.brand-tag{display:none}} | |
| 98 | +.header-spacer{flex:1} | |
| 99 | +.hbtn{border:1.5px solid var(--ink);background:var(--surface);color:var(--ink); | |
| 100 | + border-radius:var(--r-ctl);padding:8px 16px;font-family:var(--font-display); | |
| 101 | + font-size:13px;font-weight:700;transition:.15s;display:inline-flex;align-items:center; | |
| 102 | + gap:8px;white-space:nowrap;box-shadow:3px 3px 0 rgba(20,24,20,.12)} | |
| 103 | +.hbtn:hover{background:var(--lime)} | |
| 104 | +.hbtn:active{transform:translate(2px,2px);box-shadow:none} | |
| 105 | +.hbtn.primary{background:var(--ink);color:var(--lime)} | |
| 106 | +.hbtn.primary:hover{background:var(--green-deep)} | |
| 107 | +@media(max-width:760px){.header .hbtn{display:none}} | |
| 108 | + | |
| 109 | +/* ===== héros ===== */ | |
| 110 | +.hero{padding:54px 0 26px} | |
| 111 | +@media(max-width:640px){.hero{padding:30px 0 12px}} | |
| 112 | +.hero h1{font-size:clamp(30px,5.4vw,54px);font-weight:700;max-width:820px;line-height:1.06; | |
| 113 | + text-transform:uppercase} | |
| 114 | +.hero p.lead{color:var(--ink-2);font-size:clamp(14.5px,2vw,16.5px);max-width:640px;margin:18px 0 0} | |
| 115 | +.stats-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px} | |
| 116 | +.stat-pill{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 117 | + padding:8px 16px;font-family:var(--font-mono);font-size:11px;font-weight:700; | |
| 118 | + text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2); | |
| 119 | + display:inline-flex;gap:8px;align-items:center;box-shadow:3px 3px 0 rgba(20,24,20,.1)} | |
| 120 | +.stat-pill b{font-family:var(--font-display);color:var(--ink);font-size:16px;letter-spacing:0} | |
| 121 | +.advantages{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:32px 0 8px} | |
| 122 | +@media(max-width:900px){.advantages{grid-template-columns:repeat(2,1fr);gap:10px}} | |
| 123 | +.adv{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 124 | + padding:16px;box-shadow:var(--shadow-off-soft)} | |
| 125 | +.adv .ico{width:36px;height:36px;border-radius:8px;border:1.5px solid var(--ink); | |
| 126 | + background:var(--lime);color:var(--ink);display:flex;align-items:center; | |
| 127 | + justify-content:center;margin-bottom:10px;font-size:15px} | |
| 128 | +.adv:nth-child(2n) .ico{background:var(--amber-soft)} | |
| 129 | +.adv h3{font-size:14px;margin-bottom:3px} | |
| 130 | +.adv p{margin:0;font-size:12px;color:var(--ink-3);line-height:1.45} | |
| 131 | + | |
| 132 | +/* ===== recherche + filtres ===== */ | |
| 133 | +.searchbar{position:sticky;top:64px;z-index:40;background:rgba(245,243,238,.94); | |
| 134 | + backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px); | |
| 135 | + padding:12px 0;border-bottom:1.5px solid var(--ink)} | |
| 136 | +.search-wrap{display:flex;gap:10px;align-items:center} | |
| 137 | +.search-input{flex:1 1 auto;display:flex;align-items:center;gap:10px;background:var(--surface); | |
| 138 | + border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:10px 15px; | |
| 139 | + transition:.15s;min-width:0;box-shadow:3px 3px 0 rgba(20,24,20,.08)} | |
| 140 | +.search-input:focus-within{box-shadow:3px 3px 0 rgba(91,52,168,.3)} | |
| 141 | +.search-input input{border:0;background:none;outline:none;font:inherit;flex:1;color:var(--ink);min-width:0} | |
| 142 | +.search-input svg{flex:none;color:var(--ink-3)} | |
| 143 | +.filters{display:flex;gap:8px;flex-wrap:wrap} | |
| 144 | +select.f{appearance:none;-webkit-appearance:none;border:1.5px solid var(--ink); | |
| 145 | + background:var(--surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23141814' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 12px center; | |
| 146 | + border-radius:var(--r-ctl);padding:9px 30px 9px 14px;font-family:var(--font-display); | |
| 147 | + font-size:13px;font-weight:700;color:var(--ink-2);max-width:170px; | |
| 148 | + box-shadow:3px 3px 0 rgba(20,24,20,.08)} | |
| 149 | +select.f:focus{outline:2px solid var(--green);outline-offset:1px} | |
| 150 | +select.f.active{background-color:var(--lime);color:var(--ink)} | |
| 151 | +@media(max-width:760px){ | |
| 152 | + .search-wrap{flex-wrap:wrap} | |
| 153 | + .search-input{flex-basis:100%} | |
| 154 | + .search-input input{font-size:16px} | |
| 155 | + .filters{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch; | |
| 156 | + margin:0 -16px;padding:4px 16px 6px;scrollbar-width:none;width:calc(100% + 32px)} | |
| 157 | + .filters::-webkit-scrollbar{display:none} | |
| 158 | + select.f{flex:none;font-size:16px;padding:8px 30px 8px 13px} | |
| 159 | +} | |
| 160 | + | |
| 161 | +/* ===== grille créateurs ===== */ | |
| 162 | +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:18px;padding:24px 0 10px} | |
| 163 | +@media(max-width:640px){.grid{grid-template-columns:repeat(2,1fr);gap:12px}} | |
| 164 | +@media(max-width:420px){.grid{grid-template-columns:1fr;gap:14px}} | |
| 165 | +.card{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 166 | + overflow:hidden;box-shadow:var(--shadow-off-soft);transition:transform .18s ease,box-shadow .18s ease; | |
| 167 | + display:flex;flex-direction:column} | |
| 168 | +.card:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)} | |
| 169 | +.card-photo{position:relative;aspect-ratio:1/1;background: | |
| 170 | + repeating-linear-gradient(-45deg,var(--green-deep) 0 14px,#33195f 14px 28px); | |
| 171 | + display:flex;align-items:center;justify-content:center;overflow:hidden; | |
| 172 | + border-bottom:1.5px solid var(--ink)} | |
| 173 | +.card-photo .init{color:var(--lime);font-family:var(--font-display);font-weight:700; | |
| 174 | + font-size:46px;letter-spacing:.02em} | |
| 175 | +.card-photo img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} | |
| 176 | +.card-photo .tier{position:absolute;top:10px;left:10px;z-index:2} | |
| 177 | +.card-photo .nplat{position:absolute;top:10px;right:10px;z-index:2;background:var(--ink); | |
| 178 | + color:var(--lime);border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 179 | + padding:2px 8px;font-family:var(--font-mono);font-size:9.5px;font-weight:700; | |
| 180 | + text-transform:uppercase;letter-spacing:.05em} | |
| 181 | +.tier{display:inline-flex;padding:2px 9px;border:1.5px solid var(--ink); | |
| 182 | + border-radius:var(--r-ctl);font-family:var(--font-mono);font-size:9.5px; | |
| 183 | + font-weight:700;text-transform:uppercase;letter-spacing:.06em;background:var(--surface);color:var(--ink)} | |
| 184 | +.tier.mega{background:var(--lime)} | |
| 185 | +.tier.macro{background:var(--amber-soft)} | |
| 186 | +.card-body{padding:14px 15px 15px;display:flex;flex-direction:column;gap:9px;flex:1} | |
| 187 | +.card-name{font-family:var(--font-display);font-size:16px;font-weight:700; | |
| 188 | + letter-spacing:-.01em;line-height:1.25;display:flex;align-items:center;gap:6px} | |
| 189 | +.vbadge{color:var(--green);flex:none;width:15px;height:15px} | |
| 190 | +.card-sub{font-family:var(--font-mono);font-size:10px;color:var(--ink-3); | |
| 191 | + text-transform:uppercase;letter-spacing:.05em;display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:-4px} | |
| 192 | +.chips{display:flex;gap:6px;flex-wrap:wrap} | |
| 193 | +.chip{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-ctl); | |
| 194 | + padding:2px 10px;font-size:11px;font-weight:600;color:var(--ink-2);white-space:nowrap} | |
| 195 | +.reach{font-size:12px;color:var(--ink-2)} | |
| 196 | +.reach b{font-family:var(--font-display);color:var(--ink);font-size:15.5px;font-weight:700} | |
| 197 | +.platrow{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;padding-top:2px} | |
| 198 | +.plat{width:32px;height:32px;border-radius:8px;border:1.5px solid var(--ink); | |
| 199 | + display:flex;align-items:center;justify-content:center;color:#fff;transition:.15s; | |
| 200 | + box-shadow:2px 2px 0 rgba(20,24,20,.25)} | |
| 201 | +.plat svg{width:15px;height:15px;fill:currentColor} | |
| 202 | +.plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)} | |
| 203 | +.more-wrap{display:flex;justify-content:center;padding:16px 0 44px} | |
| 204 | +.empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1} | |
| 205 | +.count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase; | |
| 206 | + letter-spacing:.08em;color:var(--ink-3);padding-top:18px} | |
| 207 | +.count-line b{color:var(--ink)} | |
| 208 | + | |
| 209 | +/* ===== page créateur ===== */ | |
| 210 | +.cpage{padding:30px 0 60px} | |
| 211 | +@media(max-width:640px){.cpage{padding:20px 0 40px}} | |
| 212 | +.backlink{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono); | |
| 213 | + font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em; | |
| 214 | + color:var(--green);margin-bottom:20px} | |
| 215 | +.cpage-head{display:flex;gap:26px;align-items:center;background:var(--ink); | |
| 216 | + border:1.5px solid var(--ink);border-radius:var(--r-card);padding:32px; | |
| 217 | + color:var(--paper);box-shadow:var(--shadow-off);flex-wrap:wrap} | |
| 218 | +@media(max-width:640px){.cpage-head{padding:22px 18px;gap:16px}} | |
| 219 | +.cavatar{width:104px;height:104px;border-radius:var(--r-card); | |
| 220 | + background:repeating-linear-gradient(-45deg,var(--green-deep) 0 14px,#33195f 14px 28px); | |
| 221 | + color:var(--lime);flex:none;display:flex;align-items:center;justify-content:center; | |
| 222 | + font-family:var(--font-display);font-weight:700;font-size:36px;overflow:hidden; | |
| 223 | + position:relative;border:2px solid var(--lime)} | |
| 224 | +@media(max-width:640px){.cavatar{width:76px;height:76px;font-size:26px}} | |
| 225 | +.cavatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} | |
| 226 | +.cpage-head h1{font-size:clamp(24px,4vw,40px);font-weight:700;color:var(--paper); | |
| 227 | + line-height:1.1;display:flex;align-items:center;gap:10px;flex-wrap:wrap;text-transform:uppercase} | |
| 228 | +.cpage-head h1 .vbadge{width:22px;height:22px;color:var(--lime)} | |
| 229 | +.cpage-head .meta{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px} | |
| 230 | +.mchip{background:rgba(245,243,238,.12);border:1px solid rgba(245,243,238,.3); | |
| 231 | + border-radius:var(--r-ctl);padding:3px 11px;font-family:var(--font-mono); | |
| 232 | + font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--paper)} | |
| 233 | +.mchip.o{background:var(--lime);border-color:var(--lime);color:var(--ink)} | |
| 234 | +.cpage-reach{margin-left:auto;text-align:right} | |
| 235 | +.cpage-reach b{display:block;font-family:var(--font-display); | |
| 236 | + font-size:clamp(28px,3.6vw,40px);font-weight:700;color:var(--lime);letter-spacing:-.02em} | |
| 237 | +.cpage-reach span{font-family:var(--font-mono);font-size:10px;color:rgba(245,243,238,.6); | |
| 238 | + text-transform:uppercase;letter-spacing:.08em;font-weight:700} | |
| 239 | +@media(max-width:760px){.cpage-reach{margin-left:0;text-align:left;flex-basis:100%}} | |
| 240 | +.cpage-bio{margin:24px 0 0;max-width:820px;color:var(--ink-2);font-size:15px} | |
| 241 | +.cpage-notes{margin:14px 0 0;max-width:820px;color:var(--ink-2);font-size:12.5px; | |
| 242 | + background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 243 | + padding:13px 17px;box-shadow:var(--shadow-off-soft)} | |
| 244 | +.sect-title{font-size:19px;margin:36px 0 15px;display:flex;align-items:center;gap:10px;text-transform:uppercase} | |
| 245 | +.acc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px} | |
| 246 | +@media(max-width:640px){.acc-grid{grid-template-columns:1fr}} | |
| 247 | +.acc{display:flex;align-items:center;gap:13px;background:var(--surface); | |
| 248 | + border:1.5px solid var(--ink);border-radius:var(--r-card);padding:14px 16px; | |
| 249 | + box-shadow:var(--shadow-off-soft);transition:transform .16s ease,box-shadow .16s ease;min-width:0} | |
| 250 | +.acc:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)} | |
| 251 | +.acc .plat{width:44px;height:44px;border-radius:9px;flex:none} | |
| 252 | +.acc .plat svg{width:20px;height:20px} | |
| 253 | +.acc-mid{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px;overflow:hidden} | |
| 254 | +.acc-name{font-family:var(--font-display);font-weight:700;font-size:14px;display:flex;align-items:center;gap:6px} | |
| 255 | +.acc-handle{font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis; | |
| 256 | + white-space:nowrap;display:block;max-width:100%} | |
| 257 | +.acc-right{margin-left:auto;text-align:right;flex:none;padding-left:8px} | |
| 258 | +.acc-right b{display:block;font-family:var(--font-display);font-size:16.5px;color:var(--ink);font-weight:700} | |
| 259 | +.acc-right span{font-family:var(--font-mono);font-size:9px;color:var(--ink-3); | |
| 260 | + text-transform:uppercase;letter-spacing:.06em;font-weight:700} | |
| 261 | +.conf{display:inline-flex;padding:1px 7px;border-radius:var(--r-ctl); | |
| 262 | + font-family:var(--font-mono);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.03em} | |
| 263 | +.conf.hi{background:var(--lime-soft);color:var(--green-deep)} | |
| 264 | +.conf.mid{background:var(--amber-soft);color:#8a5a12} | |
| 265 | +.linkinbio{display:inline-flex;align-items:center;gap:10px;margin-top:22px; | |
| 266 | + background:var(--lime);color:var(--ink);border:1.5px solid var(--ink); | |
| 267 | + border-radius:var(--r-ctl);padding:11px 20px;font-family:var(--font-display); | |
| 268 | + font-weight:700;font-size:13.5px;transition:.15s;box-shadow:4px 4px 0 rgba(20,24,20,.25)} | |
| 269 | +.linkinbio:hover{background:var(--lime-soft)} | |
| 270 | +.linkinbio:active{transform:translate(2px,2px);box-shadow:none} | |
| 271 | +.linkinbio svg{width:16px;height:16px;fill:currentColor} | |
| 272 | +.cpage-foot{margin-top:38px;font-size:12px;color:var(--ink-3); | |
| 273 | + border-top:1.5px solid var(--ink);padding-top:16px} | |
| 274 | +.cpage-foot a{color:var(--green);font-weight:600} | |
| 275 | + | |
| 276 | +/* ===== page stats ===== */ | |
| 277 | +.stats-page{padding:40px 0 70px} | |
| 278 | +.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin:26px 0} | |
| 279 | +.tile{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 280 | + padding:18px 20px;box-shadow:var(--shadow-off-soft)} | |
| 281 | +.tile-v{font-family:var(--font-display);font-weight:700;font-size:clamp(23px,3vw,32px); | |
| 282 | + letter-spacing:-.03em;line-height:1.05;color:var(--ink)} | |
| 283 | +.tile-k{font-family:var(--font-mono);font-size:9.5px;font-weight:700;text-transform:uppercase; | |
| 284 | + letter-spacing:.1em;color:var(--ink-3);margin-top:7px} | |
| 285 | +.tile.hero-tile{background:var(--ink)} | |
| 286 | +.tile.hero-tile .tile-v{color:var(--lime)} | |
| 287 | +.tile.hero-tile .tile-k{color:rgba(245,243,238,.55)} | |
| 288 | +.viz-card{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card); | |
| 289 | + box-shadow:var(--shadow-off-soft);padding:24px 26px 18px;margin-bottom:22px} | |
| 290 | +.viz-card h2{font-size:18px;text-transform:uppercase} | |
| 291 | +.viz-sub{font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.08em; | |
| 292 | + text-transform:uppercase;color:var(--ink-3);margin:4px 0 18px} | |
| 293 | +.hrow{display:flex;align-items:center;gap:12px;margin-bottom:10px} | |
| 294 | +.hrow .plat{width:28px;height:28px;border-radius:7px;flex:none} | |
| 295 | +.hrow .plat svg{width:13px;height:13px} | |
| 296 | +.hrow-label{flex:0 0 130px;font-size:12.5px;font-weight:600;color:var(--ink-2); | |
| 297 | + overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| 298 | +@media(max-width:640px){.hrow-label{flex-basis:96px;font-size:11.5px}} | |
| 299 | +.hbar{flex:1;height:12px;border:1.5px solid var(--ink);border-radius:4px; | |
| 300 | + background:var(--surface-2);overflow:hidden} | |
| 301 | +.hbar i{display:block;height:100%;background:var(--lime);border-right:1.5px solid var(--ink)} | |
| 302 | +.hrow .n{flex:none;font-family:var(--font-display);font-size:12.5px;font-weight:700; | |
| 303 | + color:var(--ink);min-width:44px;text-align:right} | |
| 304 | + | |
| 305 | +/* ===== retrait ===== */ | |
| 306 | +.optout-page{max-width:620px;margin:0 auto;padding:44px 0 70px} | |
| 307 | +.optout-page h1{font-size:clamp(26px,4vw,32px);margin:12px 0;text-transform:uppercase} | |
| 308 | +.optout-page p{color:var(--ink-2)} | |
| 309 | +.field{margin-top:16px} | |
| 310 | +.field label{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700; | |
| 311 | + text-transform:uppercase;letter-spacing:.08em;margin-bottom:7px;color:var(--ink-2)} | |
| 312 | +.field input,.field textarea{width:100%;border:1.5px solid var(--ink);border-radius:var(--r-ctl); | |
| 313 | + padding:11px 14px;font:inherit;background:var(--surface);color:var(--ink); | |
| 314 | + box-shadow:3px 3px 0 rgba(20,24,20,.08);transition:box-shadow .15s ease} | |
| 315 | +.field input:focus,.field textarea:focus{outline:2px solid var(--green);outline-offset:1px; | |
| 316 | + box-shadow:3px 3px 0 rgba(91,52,168,.25)} | |
| 317 | +@media(max-width:900px){.field input,.field textarea{font-size:16px}} | |
| 318 | +.optout-ok{background:var(--lime-soft);color:var(--green-deep);border:1.5px solid var(--ink); | |
| 319 | + border-radius:var(--r-card);padding:16px 20px;font-weight:600;margin-top:20px; | |
| 320 | + box-shadow:var(--shadow-off-soft)} | |
| 321 | +.optout-ok.warn{background:var(--amber-soft);color:#8a5a12} | |
| 322 | + | |
| 323 | +/* ===== footer + tab bar mobile (patron Lou-Ka) ===== */ | |
| 324 | +.footer{border-top:1.5px solid var(--ink);margin-top:40px;padding:32px 0 44px;background:var(--ink);color:var(--paper)} | |
| 325 | +.footer-inner{display:flex;gap:26px;flex-wrap:wrap;align-items:flex-start;justify-content:space-between} | |
| 326 | +.footer .brand{font-size:19px;color:var(--paper)} | |
| 327 | +.footer .brand .ka{color:var(--ink)} | |
| 328 | +.footer p{font-size:12px;color:rgba(245,243,238,.55);max-width:460px;margin:10px 0 0} | |
| 329 | +.footer-links{display:flex;flex-direction:column;gap:9px;font-family:var(--font-mono); | |
| 330 | + font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em; | |
| 331 | + color:rgba(245,243,238,.8)} | |
| 332 | +.footer-links a:hover{color:var(--lime)} | |
| 333 | +.fam-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px} | |
| 334 | +.fam{border:1px solid rgba(245,243,238,.3);border-radius:var(--r-ctl);padding:3px 10px; | |
| 335 | + font-family:var(--font-mono);font-size:10px;font-weight:700;color:rgba(245,243,238,.7)} | |
| 336 | +.fam:hover{background:var(--lime);color:var(--ink);border-color:var(--lime)} | |
| 337 | +.tabbar{display:none} | |
| 338 | +@media(max-width:760px){ | |
| 339 | + .tabbar{display:grid;grid-template-columns:repeat(4,1fr); | |
| 340 | + position:fixed;left:0;right:0;bottom:0;z-index:85; | |
| 341 | + background:rgba(245,243,238,.94); | |
| 342 | + backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px); | |
| 343 | + border-top:1.5px solid var(--ink); | |
| 344 | + padding:6px 4px calc(6px + env(safe-area-inset-bottom))} | |
| 345 | + .tabbar a{display:flex;flex-direction:column;align-items:center;gap:3px; | |
| 346 | + padding:5px 2px;min-height:48px;justify-content:center; | |
| 347 | + font-family:var(--font-mono);font-size:9.5px;font-weight:700; | |
| 348 | + text-transform:uppercase;letter-spacing:.04em;color:var(--ink-3); | |
| 349 | + border-radius:var(--r-ctl);transition:color .12s ease} | |
| 350 | + .tabbar a.active{color:var(--green)} | |
| 351 | + .tabbar a svg{display:block;width:20px;height:20px;fill:none;stroke:currentColor; | |
| 352 | + stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round} | |
| 353 | + body{padding-bottom:calc(64px + env(safe-area-inset-bottom))} | |
| 354 | + .footer{padding-bottom:calc(76px + env(safe-area-inset-bottom))} | |
| 355 | +} | |
| 356 | +.spin{width:26px;height:26px;border:3px solid var(--line);border-top-color:var(--green); | |
| 357 | + border-radius:50%;margin:50px auto;animation:sp .8s linear infinite} | |
| 358 | +@keyframes sp{to{transform:rotate(360deg)}} | |
| 359 | +</style> | |
| 360 | +</head> | |
| 361 | +<body> | |
| 362 | +<div id="app"></div> | |
| 363 | +<script> | |
| 364 | +"use strict"; | |
| 365 | +/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /retrait */ | |
| 366 | +const ICONS=__ICONS__; | |
| 367 | +const $=s=>document.querySelector(s); | |
| 368 | +const app=$("#app"); | |
| 369 | +const fmt=new Intl.NumberFormat("fr-CA",{notation:"compact",maximumFractionDigits:1}); | |
| 370 | +const fmtFull=new Intl.NumberFormat("fr-CA"); | |
| 371 | +const PLAT={ | |
| 372 | + instagram:{label:"Instagram",color:"#c13584",av:"instagram"}, | |
| 373 | + tiktok:{label:"TikTok",color:"#141814",av:"tiktok"}, | |
| 374 | + youtube:{label:"YouTube",color:"#e02424",av:"youtube"}, | |
| 375 | + twitch:{label:"Twitch",color:"#9146ff",av:"twitch"}, | |
| 376 | + kick:{label:"Kick",color:"#141814"}, | |
| 377 | + x:{label:"X (Twitter)",color:"#141814",av:"twitter"}, | |
| 378 | + facebook:{label:"Facebook",color:"#1877f2"}, | |
| 379 | + snapchat:{label:"Snapchat",color:"#d4a900"}, | |
| 380 | + substack:{label:"Substack",color:"#ff6719"}, | |
| 381 | + patreon:{label:"Patreon",color:"#f1465a"}, | |
| 382 | + onlyfans:{label:"OnlyFans",color:"#00aff0"}, | |
| 383 | + linkedin:{label:"LinkedIn",color:"#0a66c2"}, | |
| 384 | + threads:{label:"Threads",color:"#141814"}, | |
| 385 | + podcast:{label:"Balado",color:"#8940fa"}, | |
| 386 | + "site-web":{label:"Site web",color:"#5b34a8"}, | |
| 387 | + autre:{label:"Autre",color:"#8b928c"}}; | |
| 388 | +const NICHE_LBL={"humour":"Humour","mode":"Mode","beaute":"Beauté","lifestyle":"Lifestyle", | |
| 389 | + "famille-parentalite":"Famille","cuisine":"Cuisine","gaming":"Gaming","tech":"Tech", | |
| 390 | + "sport-fitness":"Sport & fitness","plein-air":"Plein air","voyage":"Voyage","musique":"Musique", | |
| 391 | + "arts":"Arts","danse":"Danse","education":"Éducation","finance-affaires":"Finance & affaires", | |
| 392 | + "sante-mieux-etre":"Santé & mieux-être","bouffe-resto":"Bouffe & resto", | |
| 393 | + "actualite-opinion":"Actualité & opinion","autre":"Autre"}; | |
| 394 | +const TIER_LBL={nano:"Nano",micro:"Micro",macro:"Macro",mega:"Méga"}; | |
| 395 | +const state={q:"",niche:"",region:"",langue:"",plateforme:"",tier:"",sort:"reach", | |
| 396 | + offset:0,items:[],total:0,stats:null,tax:null}; | |
| 397 | + | |
| 398 | +const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); | |
| 399 | +const initials=n=>n.split(/\s+/).slice(0,2).map(w=>w[0]||"").join("").toUpperCase(); | |
| 400 | +async function api(p){const r=await fetch(p);if(!r.ok)throw new Error(r.status);return r.json()} | |
| 401 | +function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`} | |
| 402 | +const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`; | |
| 403 | + | |
| 404 | +/* cascade de photos : avatar capté à la source, puis unavatar par plateforme, | |
| 405 | + repli final = initiales sur motif encre/violet (déjà en dessous) */ | |
| 406 | +function avatarSources(c){ | |
| 407 | + const order=["instagram","tiktok","youtube","twitch","x"]; | |
| 408 | + const srcs=[]; | |
| 409 | + if(c.avatar_url)srcs.push(c.avatar_url); | |
| 410 | + for(const p of order){ | |
| 411 | + const acc=(c.platforms||[]).find(a=>a.platform===p&&PLAT[p]&&PLAT[p].av); | |
| 412 | + if(acc)srcs.push(`https://unavatar.io/${PLAT[p].av}/${encodeURIComponent(acc.handle)}?fallback=false`); | |
| 413 | + } | |
| 414 | + return srcs; | |
| 415 | +} | |
| 416 | +window.nextSrc=el=>{ | |
| 417 | + const srcs=JSON.parse(el.dataset.srcs||"[]");const i=+(el.dataset.i||0)+1; | |
| 418 | + if(i<srcs.length){el.dataset.i=i;el.src=srcs[i]}else el.remove(); | |
| 419 | +}; | |
| 420 | +function photoImg(c){ | |
| 421 | + const srcs=avatarSources(c); | |
| 422 | + if(!srcs.length)return""; | |
| 423 | + return `<img loading="lazy" alt="${esc(c.display_name)}" src="${srcs[0]}" | |
| 424 | + data-srcs='${esc(JSON.stringify(srcs))}' data-i="0" onerror="nextSrc(this)">`; | |
| 425 | +} | |
| 426 | +const brandSvg=`<svg width="30" height="30" viewBox="0 0 64 64"><rect width="64" height="64" rx="10" fill="#141814"/><circle cx="32" cy="23" r="9" fill="#c9a3ff"/><path d="M14 52c3-10 9-15 18-15s15 5 18 15" stroke="#c9a3ff" stroke-width="6" fill="none" stroke-linecap="round"/></svg>`; | |
| 427 | + | |
| 428 | +function header(){ | |
| 429 | + return `<header class="header"><div class="container header-inner"> | |
| 430 | + <a href="/" data-nav class="brand">${brandSvg}Créa<span class="ka">Ka</span></a> | |
| 431 | + <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span> | |
| 432 | + <span class="header-spacer"></span> | |
| 433 | + <a class="hbtn" href="/stats" data-nav>Statistiques</a> | |
| 434 | + <a class="hbtn" href="/retrait" data-nav>Retrait</a> | |
| 435 | + <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a> | |
| 436 | + </div></header>`; | |
| 437 | +} | |
| 438 | +function tabbar(active){ | |
| 439 | + const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`; | |
| 440 | + return `<nav class="tabbar"> | |
| 441 | + ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")} | |
| 442 | + ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")} | |
| 443 | + ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")} | |
| 444 | + ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")} | |
| 445 | + </nav>`; | |
| 446 | +} | |
| 447 | +function footer(){ | |
| 448 | + return `<footer class="footer"><div class="container footer-inner"> | |
| 449 | + <div><span class="brand">${brandSvg}Créa<span class="ka" style="background:var(--lime)">Ka</span></span> | |
| 450 | + <p>Annuaire public des créateurs de contenu québécois. Uniquement des profils publics | |
| 451 | + et professionnels ; aucune donnée privée. Retrait simple et respecté (Loi 25). | |
| 452 | + — Simon-Pierre Boucher</p> | |
| 453 | + <div class="fam-row"><span class="klabel" style="color:rgba(245,243,238,.5);align-self:center">Groupe KA :</span> | |
| 454 | + <a class="fam" href="https://www.groupe-ka.com" target="_blank" rel="noopener">KA ID</a> | |
| 455 | + <a class="fam" href="https://www.lou-ka.com" target="_blank" rel="noopener">Lou-Ka</a> | |
| 456 | + <a class="fam" href="https://www.immo-ka.com" target="_blank" rel="noopener">Immo-Ka</a> | |
| 457 | + <a class="fam" href="https://www.auto-ka.com" target="_blank" rel="noopener">Auto-Ka</a> | |
| 458 | + <a class="fam" href="https://www.food-ka.com" target="_blank" rel="noopener">Food-Ka</a> | |
| 459 | + <a class="fam" href="https://www.fabri-ka.com" target="_blank" rel="noopener">Fabri-Ka</a></div></div> | |
| 460 | + <div class="footer-links"> | |
| 461 | + <a href="/stats" data-nav>Statistiques</a> | |
| 462 | + <a href="/retrait" data-nav>Demander un retrait</a> | |
| 463 | + <a href="/api/stats" target="_blank" rel="noopener">API publique</a> | |
| 464 | + </div> | |
| 465 | + </div></footer>`; | |
| 466 | +} | |
| 467 | + | |
| 468 | +/* ---------- annuaire ---------- */ | |
| 469 | +function platIcon(a){ | |
| 470 | + const p=PLAT[a.platform]||PLAT.autre; | |
| 471 | + return `<a class="plat" href="${esc(a.url)}" target="_blank" rel="noopener" | |
| 472 | + title="${p.label} — @${esc(a.handle)}" style="background:${p.color}" | |
| 473 | + onclick="event.stopPropagation()">${icon(a.platform)}</a>`; | |
| 474 | +} | |
| 475 | +function cardHtml(c){ | |
| 476 | + const plats=(c.platforms||[]).slice(0,7).map(platIcon).join(""); | |
| 477 | + const niches=(c.niches||[]).slice(0,2).map(n=>`<span class="chip">${NICHE_LBL[n]||n}</span>`).join(""); | |
| 478 | + const verified=(c.platforms||[]).some(a=>a.verified); | |
| 479 | + const np=(c.platforms||[]).length; | |
| 480 | + return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer"> | |
| 481 | + <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)} | |
| 482 | + <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> | |
| 483 | + <span class="nplat">${np} plat.</span></div> | |
| 484 | + <div class="card-body"> | |
| 485 | + <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div> | |
| 486 | + <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div> | |
| 487 | + <div class="chips">${niches}</div> | |
| 488 | + ${c.total_reach?`<div class="reach"><b>${fmt.format(c.total_reach)}</b> abonnés cumulés</div>`:""} | |
| 489 | + <div class="platrow">${plats}</div></div></div>`; | |
| 490 | +} | |
| 491 | +function selectHtml(id,label,opts,val,lbls){ | |
| 492 | + const o=opts.map(v=>`<option value="${esc(v)}"${v===val?" selected":""}>${esc(lbls?(lbls[v]||v):v)}</option>`).join(""); | |
| 493 | + return `<select class="f${val?" active":""}" id="${id}"><option value="">${label}</option>${o}</select>`; | |
| 494 | +} | |
| 495 | +async function renderHome(){ | |
| 496 | + document.title="Créa-Ka — Tous les créateurs québécois. Tous leurs liens."; | |
| 497 | + if(!state.tax)state.tax=await api("/api/taxonomies"); | |
| 498 | + if(!state.stats)state.stats=await api("/api/stats").catch(()=>null); | |
| 499 | + const s=state.stats||{creators:0,accounts:0,by_platform:{}}; | |
| 500 | + app.innerHTML=header()+` | |
| 501 | + <section class="hero"><div class="container"> | |
| 502 | + <span class="kicker">Annuaire des créateurs du Québec</span> | |
| 503 | + <h1 style="margin-top:16px">Chaque créateur<br>québécois. <span class="outline-txt">Tous</span> ses <span class="hl">liens.</span></h1> | |
| 504 | + <p class="lead">Un « Linktree collectif » du Québec : influenceurs, youtubeurs, streamers, | |
| 505 | + podcasteurs — leurs comptes regroupés et découvrables par niche, région, langue et taille d'audience.</p> | |
| 506 | + <div class="stats-row"> | |
| 507 | + <span class="stat-pill"><b>${fmtFull.format(s.creators)}</b> créateurs</span> | |
| 508 | + <span class="stat-pill"><b>${fmtFull.format(s.accounts)}</b> comptes reliés</span> | |
| 509 | + <span class="stat-pill"><b>${Object.keys(s.by_platform||{}).length}</b> plateformes</span> | |
| 510 | + </div> | |
| 511 | + <div class="advantages"> | |
| 512 | + <div class="adv"><div class="ico">${icon("autre")}</div><h3>Cross-plateforme</h3><p>Instagram, TikTok, YouTube, Twitch, X… tous les comptes d'une même personne, au même endroit.</p></div> | |
| 513 | + <div class="adv"><div class="ico"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5" stroke-linecap="round"/></svg></div><h3>Découvrable</h3><p>Filtres par niche, région, langue, plateforme et taille d'audience.</p></div> | |
| 514 | + <div class="adv"><div class="ico">✓</div><h3>Liens vérifiés</h3><p>Chaque rattachement porte un score de confiance ; mieux vaut un lien de moins qu'un lien erroné.</p></div> | |
| 515 | + <div class="adv"><div class="ico">🛡</div><h3>Respectueux</h3><p>Profils publics seulement, retrait (opt-out) simple et respecté. Loi 25.</p></div> | |
| 516 | + </div> | |
| 517 | + </div></section> | |
| 518 | + <div class="searchbar" id="annuaire"><div class="container search-wrap"> | |
| 519 | + <label class="search-input"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/><path d="M20 20l-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> | |
| 520 | + <input id="q" placeholder="Chercher un créateur…" value="${esc(state.q)}"></label> | |
| 521 | + <div class="filters"> | |
| 522 | + ${selectHtml("f-niche","Niche",state.tax.niches,state.niche,NICHE_LBL)} | |
| 523 | + ${selectHtml("f-region","Région",state.tax.regions,state.region)} | |
| 524 | + ${selectHtml("f-plat","Plateforme",state.tax.plateformes,state.plateforme,Object.fromEntries(Object.entries(PLAT).map(([k,v])=>[k,v.label])))} | |
| 525 | + ${selectHtml("f-tier","Taille",state.tax.tiers,state.tier,TIER_LBL)} | |
| 526 | + ${selectHtml("f-langue","Langue",state.tax.langues,state.langue,{fr:"Français",en:"Anglais",bilingue:"Bilingue"})} | |
| 527 | + ${selectHtml("f-sort","Tri",["reach","nom","recent"],state.sort==="reach"?"":state.sort,{reach:"Audience",nom:"Nom A-Z",recent:"Récents"})} | |
| 528 | + </div> | |
| 529 | + </div></div> | |
| 530 | + <main class="container"><div class="count-line" id="countline"></div> | |
| 531 | + <div class="grid" id="grid"><div class="spin"></div></div> | |
| 532 | + <div class="more-wrap" id="more"></div></main>`+footer()+tabbar(location.hash==="#annuaire"?"search":"home"); | |
| 533 | + bindNav(); | |
| 534 | + $("#q").addEventListener("input",deb(()=>{state.q=$("#q").value;reload()},300)); | |
| 535 | + [["f-niche","niche"],["f-region","region"],["f-plat","plateforme"], | |
| 536 | + ["f-tier","tier"],["f-langue","langue"],["f-sort","sort"]].forEach(([id,key])=>{ | |
| 537 | + $("#"+id).addEventListener("change",e=>{state[key]=e.target.value||(key==="sort"?"reach":""); | |
| 538 | + e.target.classList.toggle("active",!!e.target.value);reload()})}); | |
| 539 | + if(location.hash==="#annuaire"){const el=$("#annuaire");if(el)el.scrollIntoView()} | |
| 540 | + await reload(); | |
| 541 | +} | |
| 542 | +function deb(fn,ms){let t;return()=>{clearTimeout(t);t=setTimeout(fn,ms)}} | |
| 543 | +async function reload(more){ | |
| 544 | + if(!more){state.offset=0;state.items=[]} | |
| 545 | + const p=new URLSearchParams({q:state.q,niche:state.niche,region:state.region, | |
| 546 | + langue:state.langue,plateforme:state.plateforme,tier:state.tier,sort:state.sort, | |
| 547 | + limit:"60",offset:String(state.offset)}); | |
| 548 | + const data=await api("/api/creators?"+p).catch(()=>({items:[],total:0})); | |
| 549 | + state.items=state.items.concat(data.items);state.total=data.total; | |
| 550 | + const grid=$("#grid");if(!grid)return; | |
| 551 | + $("#countline").innerHTML=`<b>${fmtFull.format(state.total)}</b> créateur${state.total>1?"s":""} trouvé${state.total>1?"s":""}`; | |
| 552 | + grid.innerHTML=state.items.length?state.items.map(cardHtml).join(""): | |
| 553 | + `<div class="empty">Aucun créateur ne correspond à ces filtres.</div>`; | |
| 554 | + const moreDiv=$("#more"); | |
| 555 | + moreDiv.innerHTML=state.items.length<state.total? | |
| 556 | + `<button class="btn btn-primary" id="loadmore">Afficher plus (${fmtFull.format(state.total-state.items.length)} restants)</button>`:""; | |
| 557 | + const lm=$("#loadmore");if(lm)lm.onclick=()=>{state.offset+=60;reload(true)}; | |
| 558 | + bindNav(grid); | |
| 559 | +} | |
| 560 | + | |
| 561 | +/* ---------- page créateur ---------- */ | |
| 562 | +async function renderCreator(id){ | |
| 563 | + app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("");bindNav(); | |
| 564 | + let c;try{c=await api("/api/creators/"+encodeURIComponent(id))} | |
| 565 | + catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2> | |
| 566 | + <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return} | |
| 567 | + document.title=`${c.display_name} — Créa-Ka`; | |
| 568 | + const verified=(c.platforms||[]).some(a=>a.verified); | |
| 569 | + const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join(""); | |
| 570 | + const M_LBL={following:"abonnements",posts:"publications",likes:"j'aime", | |
| 571 | + views:"vues",videos:"vidéos",episodes:"épisodes"}; | |
| 572 | + const accs=(c.platforms||[]).map(a=>{const p=PLAT[a.platform]||PLAT.autre; | |
| 573 | + const conf=(a.confidence||0)>=0.75?["hi","confiance élevée"]:["mid","à confirmer"]; | |
| 574 | + const mx=Object.entries(a.metrics||{}).filter(([k,v])=>M_LBL[k]&&v!=null) | |
| 575 | + .map(([k,v])=>`${fmt.format(v)} ${M_LBL[k]}`).join(" · "); | |
| 576 | + return `<a class="acc" href="${esc(a.url)}" target="_blank" rel="noopener"> | |
| 577 | + <span class="plat" style="background:${p.color}">${icon(a.platform)}</span> | |
| 578 | + <span class="acc-mid"><span class="acc-name">${p.label}${a.verified?checkSvg:""}</span> | |
| 579 | + <span class="acc-handle">@${esc(a.handle)}</span> | |
| 580 | + <span class="acc-handle" style="overflow:visible"><span class="conf ${conf[0]}" title="Score de rattachement ${Math.round((a.confidence||0)*100)} %">${conf[1]}</span>${mx?` <span style="font-size:11px">${mx}</span>`:""}</span></span> | |
| 581 | + <span class="acc-right">${a.followers?`<b>${fmt.format(a.followers)}</b><span>abonnés</span>`:`<span style="font-size:11px">voir le profil ↗</span>`}</span></a>`}).join(""); | |
| 582 | + const checked=(c.platforms||[]).map(a=>a.last_checked).filter(Boolean).sort().pop(); | |
| 583 | + app.innerHTML=header()+` | |
| 584 | + <div class="container cpage"> | |
| 585 | + <a class="backlink" href="/" data-nav>← Tous les créateurs</a> | |
| 586 | + <div class="cpage-head"> | |
| 587 | + <div class="cavatar">${esc(initials(c.display_name))}${photoImg(c)}</div> | |
| 588 | + <div style="flex:1;min-width:200px"><h1>${esc(c.display_name)}${verified?checkSvg:""}</h1> | |
| 589 | + <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span> | |
| 590 | + ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""} | |
| 591 | + ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div></div> | |
| 592 | + ${c.total_reach?`<div class="cpage-reach"><b>${fmt.format(c.total_reach)}</b><span>abonnés cumulés</span></div>`:""} | |
| 593 | + </div> | |
| 594 | + ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""} | |
| 595 | + ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""} | |
| 596 | + <h2 class="sect-title">Ses plateformes <span class="chip">${(c.platforms||[]).length}</span></h2> | |
| 597 | + <div class="acc-grid">${accs}</div> | |
| 598 | + ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""} | |
| 599 | + <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes. | |
| 600 | + Audiences approximatives${checked?`, dernière vérification ${esc(checked.slice(0,10))}`:""}. | |
| 601 | + Une erreur dans cette fiche ? <a href="/retrait" data-nav>Signaler / demander un retrait</a>.</div> | |
| 602 | + </div>`+footer()+tabbar(""); | |
| 603 | + bindNav();window.scrollTo(0,0); | |
| 604 | +} | |
| 605 | + | |
| 606 | +/* ---------- page stats ---------- */ | |
| 607 | +async function renderStats(){ | |
| 608 | + document.title="Statistiques — Créa-Ka"; | |
| 609 | + app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("stats");bindNav(); | |
| 610 | + const s=await api("/api/stats").catch(()=>null); | |
| 611 | + if(!s){app.innerHTML=header()+`<div class="container empty">Statistiques indisponibles.</div>`+footer()+tabbar("stats");bindNav();return} | |
| 612 | + const bars=(obj,lbls,useIcon)=>{const entries=Object.entries(obj).slice(0,12); | |
| 613 | + const max=Math.max(...entries.map(e=>e[1]),1); | |
| 614 | + return entries.map(([k,v])=>`<div class="hrow"> | |
| 615 | + ${useIcon?`<span class="plat" style="background:${(PLAT[k]||PLAT.autre).color}">${icon(k)}</span>`:""} | |
| 616 | + <span class="hrow-label">${esc(lbls?(lbls[k]||k):k)}</span> | |
| 617 | + <span class="hbar"><i style="width:${Math.max(2,Math.round(v/max*100))}%"></i></span> | |
| 618 | + <span class="n">${fmtFull.format(v)}</span></div>`).join("")}; | |
| 619 | + app.innerHTML=header()+` | |
| 620 | + <div class="container stats-page"> | |
| 621 | + <span class="kicker">Portrait de l'annuaire</span> | |
| 622 | + <h1 style="font-size:clamp(28px,4.6vw,46px);margin:12px 0 4px;text-transform:uppercase">Les créateurs québécois <span class="hl">en chiffres.</span></h1> | |
| 623 | + <div class="tiles"> | |
| 624 | + <div class="tile hero-tile"><div class="tile-v">${fmtFull.format(s.creators)}</div><div class="tile-k">Créateurs</div></div> | |
| 625 | + <div class="tile"><div class="tile-v">${fmtFull.format(s.accounts)}</div><div class="tile-k">Comptes reliés</div></div> | |
| 626 | + <div class="tile"><div class="tile-v">${Object.keys(s.by_platform||{}).length}</div><div class="tile-k">Plateformes</div></div> | |
| 627 | + <div class="tile"><div class="tile-v">${(s.accounts/Math.max(1,s.creators)).toFixed(1)}</div><div class="tile-k">Comptes / créateur</div></div> | |
| 628 | + </div> | |
| 629 | + <div class="viz-card"><h2>Par plateforme</h2><div class="viz-sub">comptes publics reliés</div> | |
| 630 | + ${bars(s.by_platform,Object.fromEntries(Object.entries(PLAT).map(([k,v])=>[k,v.label])),true)}</div> | |
| 631 | + <div class="viz-card"><h2>Par niche</h2><div class="viz-sub">créateurs actifs</div> | |
| 632 | + ${bars(s.by_niche,NICHE_LBL)}</div> | |
| 633 | + <div class="viz-card"><h2>Par taille d'audience</h2><div class="viz-sub">basée sur la plateforme principale</div> | |
| 634 | + ${bars(s.by_tier,TIER_LBL)}</div> | |
| 635 | + ${Object.keys(s.by_region||{}).length?`<div class="viz-card"><h2>Par région déclarée</h2><div class="viz-sub">quand le créateur la rend publique</div>${bars(s.by_region)}</div>`:""} | |
| 636 | + </div>`+footer()+tabbar("stats"); | |
| 637 | + bindNav();window.scrollTo(0,0); | |
| 638 | +} | |
| 639 | + | |
| 640 | +/* ---------- retrait / opt-out ---------- */ | |
| 641 | +function renderOptout(){ | |
| 642 | + document.title="Retrait (opt-out) — Créa-Ka"; | |
| 643 | + app.innerHTML=header()+` | |
| 644 | + <div class="container optout-page"> | |
| 645 | + <span class="kicker">Vie privée d'abord</span> | |
| 646 | + <h1>Demander un <span class="hl">retrait</span></h1> | |
| 647 | + <p>Vous êtes créateur ou créatrice et souhaitez ne plus figurer dans l'annuaire ? | |
| 648 | + La fiche est <b>masquée immédiatement</b> et exclue de toute ré-agrégation future. | |
| 649 | + Une correction d'erreur (mauvais compte rattaché) passe par le même formulaire.</p> | |
| 650 | + <form id="of"> | |
| 651 | + <div class="field"><label>Nom public / nom de scène</label><input id="o-name" maxlength="200" placeholder="ex. Prénom Nom"></div> | |
| 652 | + <div class="field"><label>Un de vos comptes (plateforme:handle)</label><input id="o-acc" maxlength="200" placeholder="ex. instagram:monhandle"></div> | |
| 653 | + <div class="field"><label>Raison (facultatif)</label><textarea id="o-reason" rows="3" maxlength="500"></textarea></div> | |
| 654 | + <div class="field"><label>Courriel de contact (facultatif, pour confirmation)</label><input id="o-contact" maxlength="200" type="email"></div> | |
| 655 | + <div class="field"><button class="btn btn-primary" type="submit">Masquer ma fiche</button></div> | |
| 656 | + </form><div id="o-result"></div> | |
| 657 | + </div>`+footer()+tabbar("optout"); | |
| 658 | + bindNav(); | |
| 659 | + $("#of").addEventListener("submit",async e=>{e.preventDefault(); | |
| 660 | + const body={name:$("#o-name").value.trim(),account:$("#o-acc").value.trim(), | |
| 661 | + reason:$("#o-reason").value.trim(),contact:$("#o-contact").value.trim()}; | |
| 662 | + if(!body.name&&!body.account){$("#o-result").innerHTML=`<div class="optout-ok warn">Indiquez au moins un nom ou un compte.</div>`;return} | |
| 663 | + try{const r=await fetch("/api/optout",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}); | |
| 664 | + const j=await r.json(); | |
| 665 | + $("#o-result").innerHTML=`<div class="optout-ok">✓ ${esc(j.message||"Demande enregistrée.")} ${j.masked?`(${j.masked} fiche(s) masquée(s))`:""}</div>`; | |
| 666 | + e.target.reset();} | |
| 667 | + catch(err){$("#o-result").innerHTML=`<div class="optout-ok warn">Erreur — réessayez ou écrivez à contact@spboucher.ai.</div>`}}); | |
| 668 | +} | |
| 669 | + | |
| 670 | +/* ---------- routeur ---------- */ | |
| 671 | +function bindNav(root){ | |
| 672 | + (root||document).querySelectorAll("[data-nav]").forEach(a=>{ | |
| 673 | + if(a._nav)return;a._nav=1; | |
| 674 | + a.addEventListener("click",e=>{const href=a.getAttribute("href"); | |
| 675 | + e.preventDefault();history.pushState(null,"",href);route(); | |
| 676 | + if(href.endsWith("#annuaire")){const el=$("#annuaire");if(el){el.scrollIntoView();const q=$("#q");if(q)q.focus()}}})}); | |
| 677 | + (root||document).querySelectorAll("[data-href]").forEach(card=>{ | |
| 678 | + if(card._nav)return;card._nav=1; | |
| 679 | + const go=()=>{history.pushState(null,"",card.dataset.href);route()}; | |
| 680 | + card.addEventListener("click",e=>{if(e.target.closest("a"))return;go()}); | |
| 681 | + card.addEventListener("keydown",e=>{if(e.key==="Enter")go()}); | |
| 682 | + });} | |
| 683 | +function route(){ | |
| 684 | + const p=location.pathname; | |
| 685 | + const m=p.match(/^\/createur\/([^/]+)$/); | |
| 686 | + if(m)return renderCreator(decodeURIComponent(m[1])); | |
| 687 | + if(p==="/retrait")return renderOptout(); | |
| 688 | + if(p==="/stats")return renderStats(); | |
| 689 | + return renderHome(); | |
| 690 | +} | |
| 691 | +window.addEventListener("popstate",route); | |
| 692 | +route(); | |
| 693 | +</script> | |
| 694 | +</body> | |
| 695 | +</html> | |
added
requirements.txt
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: requirements.txt | |
| 4 | +# Desc: Dépendances backend Créa-Ka | |
| 5 | +# ============================================================================== | |
| 6 | +fastapi>=0.110 | |
| 7 | +uvicorn>=0.29 | |
| 8 | +requests>=2.31 | |
| 9 | +pydantic>=2.5 | |
| 10 | +pytest>=8.0 | |
added
run.py
+62 −0
@@ -0,0 +1,62 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# ============================================================================== | |
| 3 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 4 | +# File: run.py | |
| 5 | +# Desc: Point d'entrée Créa-Ka — `sync`, `watch`, `serve` (calqué sur Lou-Ka) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Utilisation : | |
| 8 | + python run.py sync [source ...] # synchronise (découverte + enrichissement) | |
| 9 | + python run.py watch [heures] # synchronise en boucle (défaut 24 h) | |
| 10 | + python run.py serve [port] # démarre l'API + le frontend (défaut 8160) | |
| 11 | + python run.py optout <nom|plateforme:handle> # retrait manuel immédiat | |
| 12 | +""" | |
| 13 | +from __future__ import annotations | |
| 14 | + | |
| 15 | +import os | |
| 16 | +import sys | |
| 17 | +from pathlib import Path | |
| 18 | + | |
| 19 | +# Charger .env (SCRAPFLY_KEY, FIRECRAWL_API_KEY, YOUTUBE_API_KEY…) sans dépendance | |
| 20 | +_env = Path(__file__).parent / ".env" | |
| 21 | +if _env.exists(): | |
| 22 | + for line in _env.read_text().splitlines(): | |
| 23 | + line = line.strip() | |
| 24 | + if line and not line.startswith("#") and "=" in line: | |
| 25 | + k, _, v = line.partition("=") | |
| 26 | + os.environ.setdefault(k.strip(), v.strip()) | |
| 27 | + | |
| 28 | + | |
| 29 | +def main() -> None: | |
| 30 | + cmd = sys.argv[1] if len(sys.argv) > 1 else "serve" | |
| 31 | + if cmd == "sync": | |
| 32 | + from creaka import ingest | |
| 33 | + ingest.run(sys.argv[2:] or None) | |
| 34 | + elif cmd == "watch": | |
| 35 | + from creaka import ingest | |
| 36 | + hours = float(sys.argv[2]) if len(sys.argv) > 2 else 24.0 | |
| 37 | + ingest.watch(int(hours * 3600)) | |
| 38 | + elif cmd == "optout": | |
| 39 | + if len(sys.argv) < 3: | |
| 40 | + print(__doc__) | |
| 41 | + sys.exit(1) | |
| 42 | + from creaka import db, ethics | |
| 43 | + target = sys.argv[2] | |
| 44 | + is_account = ":" in target | |
| 45 | + ethics.add_optout(name=None if is_account else target, | |
| 46 | + account=target if is_account else None, | |
| 47 | + reason="retrait manuel (CLI)") | |
| 48 | + n = db.apply_optout(db.connect(), | |
| 49 | + name=None if is_account else target, | |
| 50 | + account=target if is_account else None) | |
| 51 | + print(f"[crea-ka] opt-out enregistré, {n} fiche(s) masquée(s)") | |
| 52 | + elif cmd == "serve": | |
| 53 | + import uvicorn | |
| 54 | + port = int(sys.argv[2]) if len(sys.argv) > 2 else 8160 | |
| 55 | + uvicorn.run("creaka.web:app", host="0.0.0.0", port=port) | |
| 56 | + else: | |
| 57 | + print(__doc__) | |
| 58 | + sys.exit(1) | |
| 59 | + | |
| 60 | + | |
| 61 | +if __name__ == "__main__": | |
| 62 | + main() | |
added
scripts/build_frontend.py
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: scripts/build_frontend.py | |
| 4 | +# Desc: Construit frontend/dist/index.html à partir du gabarit en injectant | |
| 5 | +# les tracés SVG des marques (simple-icons, licence CC0) | |
| 6 | +# ============================================================================== | |
| 7 | +"""Usage : python3 scripts/build_frontend.py | |
| 8 | + | |
| 9 | +Lit frontend/src/index.template.html + frontend/src/icons.json et produit | |
| 10 | +frontend/dist/index.html (remplacement du marqueur __ICONS__).""" | |
| 11 | +from __future__ import annotations | |
| 12 | + | |
| 13 | +import json | |
| 14 | +from pathlib import Path | |
| 15 | + | |
| 16 | +ROOT = Path(__file__).resolve().parent.parent | |
| 17 | +SRC = ROOT / "frontend" / "src" | |
| 18 | +DIST = ROOT / "frontend" / "dist" | |
| 19 | + | |
| 20 | + | |
| 21 | +def main() -> None: | |
| 22 | + icons = json.loads((SRC / "icons.json").read_text(encoding="utf-8")) | |
| 23 | + html = (SRC / "index.template.html").read_text(encoding="utf-8") | |
| 24 | + html = html.replace("__ICONS__", json.dumps(icons, separators=(",", ":"))) | |
| 25 | + DIST.mkdir(parents=True, exist_ok=True) | |
| 26 | + (DIST / "index.html").write_text(html, encoding="utf-8") | |
| 27 | + print(f"[crea-ka] frontend construit → {DIST/'index.html'} " | |
| 28 | + f"({len(html)//1024} Ko, {len(icons)} icônes)") | |
| 29 | + | |
| 30 | + | |
| 31 | +if __name__ == "__main__": | |
| 32 | + main() | |
added
scripts/deploy-m3u96b.sh
+43 −0
@@ -0,0 +1,43 @@ | ||
| 1 | +#!/bin/zsh | |
| 2 | +# ============================================================================== | |
| 3 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 4 | +# File: scripts/deploy-m3u96b.sh | |
| 5 | +# Desc: Déploiement/redéploiement de Créa-Ka sur le node M3U96b — | |
| 6 | +# rsync + venv + PM2 (web :8160, sync watch 24 h, ngrok www.crea-ka.com) | |
| 7 | +# ============================================================================== | |
| 8 | +set -euo pipefail | |
| 9 | + | |
| 10 | +NODE=M3U96b | |
| 11 | +# ⚠️ openrsync (macOS) : TOUJOURS un chemin distant ABSOLU — la forme | |
| 12 | +# « hôte:chemin/relatif » est interprétée comme un chemin LOCAL (bug silencieux) | |
| 13 | +APP_DIR=/Users/simon-pierreboucher/apps/crea-ka | |
| 14 | +SRC="$(cd "$(dirname "$0")/.." && pwd)" | |
| 15 | +PORT=8160 | |
| 16 | +DOMAIN=www.crea-ka.com | |
| 17 | + | |
| 18 | +echo "[deploy] rsync → $NODE:$APP_DIR" | |
| 19 | +ssh "$NODE" "mkdir -p ~/apps/crea-ka" | |
| 20 | +# .env et base de données du node préservés (--update pour la DB seulement au 1er envoi) | |
| 21 | +rsync -az --delete \ | |
| 22 | + --exclude '.venv' --exclude '__pycache__' --exclude '.pytest_cache' \ | |
| 23 | + --exclude '.env' --exclude 'data/creaka.db*' \ | |
| 24 | + --exclude 'data/optout.json' --exclude 'data/optout_requests.jsonl' \ | |
| 25 | + "$SRC/" "$NODE:$APP_DIR/" | |
| 26 | + | |
| 27 | +echo "[deploy] venv + dépendances" | |
| 28 | +ssh "$NODE" "cd ~/apps/crea-ka && \ | |
| 29 | + ([ -d .venv ] || python3 -m venv .venv) && \ | |
| 30 | + .venv/bin/pip -q install -r requirements.txt" | |
| 31 | + | |
| 32 | +echo "[deploy] PM2 (web :$PORT + sync + ngrok $DOMAIN)" | |
| 33 | +ssh "$NODE" "cd ~/apps/crea-ka && \ | |
| 34 | + pm2 delete crea-ka-web crea-ka-sync crea-ka-ngrok 2>/dev/null; \ | |
| 35 | + pm2 start .venv/bin/python --name crea-ka-web --interpreter none --cwd ~/apps/crea-ka -- run.py serve $PORT && \ | |
| 36 | + PYTHONUNBUFFERED=1 pm2 start .venv/bin/python --name crea-ka-sync --interpreter none --cwd ~/apps/crea-ka -- run.py watch 24 && \ | |
| 37 | + pm2 start /opt/homebrew/bin/ngrok --name crea-ka-ngrok --interpreter none -- http --url=$DOMAIN $PORT && \ | |
| 38 | + pm2 save" | |
| 39 | + | |
| 40 | +echo "[deploy] healthcheck" | |
| 41 | +sleep 4 | |
| 42 | +ssh "$NODE" "curl -sf localhost:$PORT/healthz" && echo " ← local OK" | |
| 43 | +curl -sf "https://$DOMAIN/healthz" && echo " ← https://$DOMAIN OK" | |
added
scripts/screenshots.sh
+20 −0
@@ -0,0 +1,20 @@ | ||
| 1 | +#!/bin/zsh | |
| 2 | +# ============================================================================== | |
| 3 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 4 | +# File: scripts/screenshots.sh | |
| 5 | +# Desc: Captures d'écran du site en production pour le README | |
| 6 | +# (Playwright CLI, Chromium headless) → docs/screenshots/ | |
| 7 | +# ============================================================================== | |
| 8 | +set -euo pipefail | |
| 9 | +BASE=${1:-https://www.crea-ka.com} | |
| 10 | +OUT="$(cd "$(dirname "$0")/.." && pwd)/docs/screenshots" | |
| 11 | +mkdir -p "$OUT" | |
| 12 | +PW="npx -y playwright@1.55 screenshot --wait-for-timeout 6000" | |
| 13 | + | |
| 14 | +eval $PW --viewport-size=1440,900 "$BASE/" "$OUT/accueil.png" | |
| 15 | +eval $PW --viewport-size=1440,900 --full-page "$BASE/stats" "$OUT/stats.png" | |
| 16 | +eval $PW --viewport-size=390,844 "$BASE/" "$OUT/mobile.png" | |
| 17 | +# page créateur : premier résultat de l'annuaire | |
| 18 | +CID=$(curl -s "$BASE/api/creators?limit=1" | python3 -c "import json,sys;print(json.load(sys.stdin)['items'][0]['id'])") | |
| 19 | +eval $PW --viewport-size=1440,900 "$BASE/createur/$CID" "$OUT/createur.png" | |
| 20 | +echo "[crea-ka] captures → $OUT" | |
added
tests/fixtures/linktree_sample.html
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +<!-- | |
| 2 | + Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | + File: tests/fixtures/linktree_sample.html | |
| 4 | + Desc: Fixture figée — page Linktree publique simplifiée (structure Next.js | |
| 5 | + réelle : liens dans __NEXT_DATA__ + ancres de repli) | |
| 6 | +--> | |
| 7 | +<!DOCTYPE html> | |
| 8 | +<html><head><title>@creatrice-exemple | Linktree</title></head> | |
| 9 | +<body> | |
| 10 | +<div id="__next"> | |
| 11 | + <a href="https://www.instagram.com/creatrice.exemple/">Instagram</a> | |
| 12 | + <a href="https://linktr.ee/discover">Découvrir Linktree</a> | |
| 13 | +</div> | |
| 14 | +<script id="__NEXT_DATA__" type="application/json"> | |
| 15 | +{"props":{"pageProps":{"account":{"username":"creatrice-exemple", | |
| 16 | +"links":[ | |
| 17 | + {"id":1,"title":"Ma chaîne YouTube","url":"https://www.youtube.com/@CreatriceExemple"}, | |
| 18 | + {"id":2,"title":"TikTok","url":"https://www.tiktok.com/@creatrice.exemple"}, | |
| 19 | + {"id":3,"title":"Twitch","url":"https://twitch.tv/creatriceexemple"}, | |
| 20 | + {"id":4,"title":"Mon Patreon","url":"https://www.patreon.com/creatriceexemple"}, | |
| 21 | + {"id":5,"title":"Boutique","url":"https://boutique-exemple.com/produits"}, | |
| 22 | + {"id":6,"title":"Un reel","url":"https://www.instagram.com/reel/xyz123/"} | |
| 23 | +],"socialLinks":[{"type":"x","url":"https://x.com/creatrice_ex"}]}}}} | |
| 24 | +</script> | |
| 25 | +</body></html> | |
added
tests/test_ethics.py
+76 −0
@@ -0,0 +1,76 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: tests/test_ethics.py | |
| 4 | +# Desc: Contrôle éthique automatisé (§18) — aucune donnée privée en base, | |
| 5 | +# régime des mineurs, opt-out persistant et respecté | |
| 6 | +# ============================================================================== | |
| 7 | +import json | |
| 8 | + | |
| 9 | +from creaka import db, ethics | |
| 10 | +from creaka.identity import account | |
| 11 | +from creaka.schema import Creator | |
| 12 | + | |
| 13 | + | |
| 14 | +def _creator(name, accounts, **kw): | |
| 15 | + return Creator(source="test", external_id="", display_name=name, | |
| 16 | + platforms=accounts, **kw).finalize() | |
| 17 | + | |
| 18 | + | |
| 19 | +def test_scrub_donnees_privees(): | |
| 20 | + cr = _creator("Nom Public", [account("instagram", "nompublic", "listes_medias")], | |
| 21 | + bio="Rejoins-moi au 514 555-1234 ou perso@gmail.com, " | |
| 22 | + "123, rue Exemple app. 4") | |
| 23 | + ethics.scrub(cr) | |
| 24 | + assert "514" not in cr.bio | |
| 25 | + assert "@gmail" not in cr.bio | |
| 26 | + assert "rue Exemple" not in cr.bio | |
| 27 | + | |
| 28 | + | |
| 29 | +def test_courriel_perso_jamais_conserve(): | |
| 30 | + cr = _creator("Nom Public", [account("instagram", "nompublic", "listes_medias")], | |
| 31 | + business_contact="perso@hotmail.com") | |
| 32 | + ethics.scrub(cr) | |
| 33 | + assert cr.business_contact is None | |
| 34 | + cr2 = _creator("Nom Public", [account("instagram", "nompublic", "listes_medias")], | |
| 35 | + business_contact="booking@agence-exemple.ca") | |
| 36 | + ethics.scrub(cr2) | |
| 37 | + assert cr2.business_contact == "booking@agence-exemple.ca" # pro affiché → OK | |
| 38 | + | |
| 39 | + | |
| 40 | +def test_regime_mineur(): | |
| 41 | + cr = _creator("Jeune Créateur", [account("tiktok", "jeune", "listes_medias")], | |
| 42 | + is_minor=True, region="Montréal", city="Montréal", | |
| 43 | + business_contact="pro@agence.ca") | |
| 44 | + ethics.scrub(cr) | |
| 45 | + assert cr.business_contact is None | |
| 46 | + assert cr.region is None and cr.city is None | |
| 47 | + | |
| 48 | + | |
| 49 | +def test_optout_persistant_et_exclu(tmp_path, monkeypatch): | |
| 50 | + monkeypatch.setattr(ethics, "OPTOUT_PATH", tmp_path / "optout.json") | |
| 51 | + monkeypatch.setattr(ethics, "OPTOUT_REQUESTS_PATH", tmp_path / "req.jsonl") | |
| 52 | + con = db.connect(tmp_path / "test.db") | |
| 53 | + cr = _creator("Créatrice Retrait", | |
| 54 | + [account("instagram", "retrait.exemple", "listes_medias")]) | |
| 55 | + db.sync_source(con, "test", [cr]) | |
| 56 | + assert db.search(con, q="retrait")["total"] == 1 | |
| 57 | + # demande de retrait → masquée immédiatement | |
| 58 | + ethics.add_optout(account="instagram:retrait.exemple", reason="test") | |
| 59 | + assert db.apply_optout(con, account="instagram:retrait.exemple") == 1 | |
| 60 | + assert db.search(con, q="retrait")["total"] == 0 | |
| 61 | + # ré-agrégation future → JAMAIS réintégrée (liste de suppression, §15) | |
| 62 | + cr2 = _creator("Créatrice Retrait", | |
| 63 | + [account("instagram", "retrait.exemple", "link_in_bio")]) | |
| 64 | + stats = db.sync_source(con, "test", [cr2]) | |
| 65 | + assert stats["optout_skipped"] == 1 | |
| 66 | + assert db.search(con, q="retrait")["total"] == 0 | |
| 67 | + persisted = json.loads((tmp_path / "optout.json").read_text()) | |
| 68 | + assert "instagram:retrait.exemple" in persisted["accounts"] | |
| 69 | + | |
| 70 | + | |
| 71 | +def test_mineurs_exclus_de_lannuaire_public(tmp_path): | |
| 72 | + con = db.connect(tmp_path / "test2.db") | |
| 73 | + cr = _creator("Jeune Vedette", [account("tiktok", "jeunevedette", "listes_medias")], | |
| 74 | + is_minor=True) | |
| 75 | + db.sync_source(con, "test", [cr]) | |
| 76 | + assert db.search(con)["total"] == 0 # prudence extrême §15 | |
added
tests/test_identity.py
+63 −0
@@ -0,0 +1,63 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: tests/test_identity.py | |
| 4 | +# Desc: Tests de résolution d'identité & déduplication — cas multi-plateformes | |
| 5 | +# + cas-pièges d'HOMONYMES à ne jamais fusionner (§12, §18) | |
| 6 | +# ============================================================================== | |
| 7 | +from creaka.dedup import dedupe, merge_creators, should_merge | |
| 8 | +from creaka.identity import account, merge_accounts | |
| 9 | +from creaka.schema import Creator | |
| 10 | + | |
| 11 | + | |
| 12 | +def _creator(name, accounts, **kw): | |
| 13 | + return Creator(source="test", external_id="", display_name=name, | |
| 14 | + platforms=accounts, **kw).finalize() | |
| 15 | + | |
| 16 | + | |
| 17 | +def test_homonymes_jamais_fusionnes(): | |
| 18 | + """Deux personnes au même nom SANS compte partagé → deux fiches (§12.2).""" | |
| 19 | + a = _creator("Alex Tremblay", [account("instagram", "alex.gaming", "listes_medias")]) | |
| 20 | + b = _creator("Alex Tremblay", [account("instagram", "alexcuisine", "listes_medias")]) | |
| 21 | + assert not should_merge(a, b) | |
| 22 | + assert len(dedupe([a, b])) == 2 | |
| 23 | + | |
| 24 | + | |
| 25 | +def test_fusion_sur_compte_fort_partage(): | |
| 26 | + a = _creator("Nom Scène", [account("instagram", "nomscene", "listes_medias"), | |
| 27 | + account("youtube", "nomscene", "listes_medias")]) | |
| 28 | + b = _creator("Nom Scene", [account("instagram", "nomscene", "link_in_bio"), | |
| 29 | + account("twitch", "nomscene_tv", "link_in_bio")]) | |
| 30 | + assert should_merge(a, b) | |
| 31 | + merged = dedupe([a, b]) | |
| 32 | + assert len(merged) == 1 | |
| 33 | + keys = {acc.key for acc in merged[0].platforms} | |
| 34 | + # union : on ne perd AUCUN lien vérifié | |
| 35 | + assert keys == {"instagram:nomscene", "youtube:nomscene", "twitch:nomscene_tv"} | |
| 36 | + | |
| 37 | + | |
| 38 | +def test_pas_de_fusion_sur_compte_faible(): | |
| 39 | + """Un compte partagé à confiance basse ne suffit pas (sous STRONG_THRESHOLD).""" | |
| 40 | + a = _creator("Personne Un", [account("instagram", "commun", "mention"), | |
| 41 | + account("youtube", "personneun", "listes_medias")]) | |
| 42 | + b = _creator("Personne Deux", [account("instagram", "commun", "mention"), | |
| 43 | + account("tiktok", "personnedeux", "listes_medias")]) | |
| 44 | + assert not should_merge(a, b) | |
| 45 | + | |
| 46 | + | |
| 47 | +def test_merge_accounts_meilleure_confiance(): | |
| 48 | + old = [account("instagram", "nom", "handle_match", followers=1000).finalize()] | |
| 49 | + new = [account("instagram", "nom", "link_in_bio").finalize()] | |
| 50 | + merged = merge_accounts(old, new) | |
| 51 | + assert len(merged) == 1 | |
| 52 | + assert merged[0].confidence == 0.95 # link_in_bio gagne | |
| 53 | + assert merged[0].followers == 1000 # métrique conservée | |
| 54 | + | |
| 55 | + | |
| 56 | +def test_merge_creators_regime_mineur_et_optout(): | |
| 57 | + a = _creator("Jeune Talent", [account("tiktok", "jeune", "listes_medias")], | |
| 58 | + region="Montréal", city="Montréal") | |
| 59 | + b = _creator("Jeune Talent", [account("tiktok", "jeune", "link_in_bio")], | |
| 60 | + is_minor=True) | |
| 61 | + merged = merge_creators(a, b) | |
| 62 | + assert merged.is_minor is True | |
| 63 | + assert merged.region is None and merged.city is None # régime restreint §15 | |
added
tests/test_linkinbio.py
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: tests/test_linkinbio.py | |
| 4 | +# Desc: Tests du connecteur générique link-in-bio sur fixture réelle figée — | |
| 5 | +# extraction des comptes avec signal fort (0.95) | |
| 6 | +# ============================================================================== | |
| 7 | +from pathlib import Path | |
| 8 | + | |
| 9 | +from creaka.connectors.linkinbio import extract_accounts, is_supported, parse_links | |
| 10 | + | |
| 11 | +FIXTURE = (Path(__file__).parent / "fixtures" / "linktree_sample.html").read_text() | |
| 12 | + | |
| 13 | + | |
| 14 | +def test_is_supported(): | |
| 15 | + assert is_supported("https://linktr.ee/nom") | |
| 16 | + assert is_supported("https://beacons.ai/nom") | |
| 17 | + assert not is_supported("https://exemple.com/nom") | |
| 18 | + assert not is_supported(None) | |
| 19 | + | |
| 20 | + | |
| 21 | +def test_parse_links_next_data_et_ancres(): | |
| 22 | + links = parse_links(FIXTURE) | |
| 23 | + assert "https://www.youtube.com/@CreatriceExemple" in links | |
| 24 | + assert "https://www.instagram.com/creatrice.exemple" in links # ancre de repli | |
| 25 | + | |
| 26 | + | |
| 27 | +def test_extract_accounts_signal_fort(): | |
| 28 | + accounts = extract_accounts(FIXTURE) | |
| 29 | + by_platform = {a.platform: a for a in accounts} | |
| 30 | + assert by_platform["youtube"].handle == "creatriceexemple" | |
| 31 | + assert by_platform["tiktok"].handle == "creatrice.exemple" | |
| 32 | + assert by_platform["twitch"].handle == "creatriceexemple" | |
| 33 | + assert by_platform["patreon"].handle == "creatriceexemple" | |
| 34 | + assert by_platform["x"].handle == "creatrice_ex" | |
| 35 | + assert all(a.confidence == 0.95 for a in accounts) # signal link_in_bio | |
| 36 | + assert all(a.signal == "link_in_bio" for a in accounts) | |
| 37 | + # la boutique (site marchand) et le reel (page générique) ne sont PAS des comptes | |
| 38 | + assert "site-web" not in by_platform or "boutique" not in \ | |
| 39 | + by_platform.get("site-web").handle | |
added
tests/test_normalize.py
+82 −0
@@ -0,0 +1,82 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: tests/test_normalize.py | |
| 4 | +# Desc: Tests de normalisation — handles, URLs canoniques, niches, régions, | |
| 5 | +# compteurs d'abonnés, taille d'audience | |
| 6 | +# ============================================================================== | |
| 7 | +from creaka.normalize import ( | |
| 8 | + audience_tier, canonical_url, clean_handle, map_niche, map_region, | |
| 9 | + parse_count, platform_from_url, | |
| 10 | +) | |
| 11 | + | |
| 12 | + | |
| 13 | +def test_clean_handle(): | |
| 14 | + assert clean_handle("@Enola.Bedard") == "enola.bedard" | |
| 15 | + assert clean_handle("https://www.instagram.com/enola.bedard/") == "enola.bedard" | |
| 16 | + assert clean_handle(" @Nom_Scene ") == "nom_scene" | |
| 17 | + assert clean_handle(None) == "" | |
| 18 | + | |
| 19 | + | |
| 20 | +def test_youtube_channel_id(): | |
| 21 | + # ID de chaîne : casse préservée + forme /channel/ (jamais @ucxxx minuscule) | |
| 22 | + cid = "UCtH1VriJJmhYkGSbq_7Jhra"[:24] | |
| 23 | + assert clean_handle(cid) == cid | |
| 24 | + assert canonical_url("youtube", cid) == f"https://www.youtube.com/channel/{cid}" | |
| 25 | + | |
| 26 | + | |
| 27 | +def test_canonical_url(): | |
| 28 | + assert canonical_url("instagram", "nom") == "https://www.instagram.com/nom/" | |
| 29 | + assert canonical_url("tiktok", "@Nom") == "https://www.tiktok.com/@nom" | |
| 30 | + assert canonical_url("substack", "infolettre") == "https://infolettre.substack.com" | |
| 31 | + assert canonical_url("onlyfans", "nom") == "https://onlyfans.com/nom" | |
| 32 | + # pas de forme canonique → repli sur l'URL source | |
| 33 | + assert canonical_url("site-web", "x", fallback="https://exemple.com") == \ | |
| 34 | + "https://exemple.com" | |
| 35 | + | |
| 36 | + | |
| 37 | +def test_platform_from_url(): | |
| 38 | + assert platform_from_url("https://www.tiktok.com/@nom") == ("tiktok", "nom") | |
| 39 | + assert platform_from_url("https://twitter.com/nom") == ("x", "nom") | |
| 40 | + assert platform_from_url("https://mono.substack.com") == ("substack", "mono") | |
| 41 | + # pages génériques ≠ handles | |
| 42 | + assert platform_from_url("https://www.instagram.com/p/abc123/") is None | |
| 43 | + assert platform_from_url("https://www.youtube.com/watch?v=x") is None | |
| 44 | + assert platform_from_url("https://exemple.com/page") is None | |
| 45 | + | |
| 46 | + | |
| 47 | +def test_map_niche(): | |
| 48 | + assert map_niche("food") == "bouffe-resto" | |
| 49 | + assert map_niche("Beauté") == "beaute" | |
| 50 | + assert map_niche("fitness") == "sport-fitness" | |
| 51 | + assert map_niche("entrepreneur") == "finance-affaires" | |
| 52 | + assert map_niche("catégorie-inconnue") == "autre" | |
| 53 | + | |
| 54 | + | |
| 55 | +def test_map_region_prudence(): | |
| 56 | + # « Québec » seul = la PROVINCE → jamais de région inférée (§7) | |
| 57 | + assert map_region("Québec") == (None, None) | |
| 58 | + assert map_region("Québec (ville)") == ("Capitale-Nationale", "Québec") | |
| 59 | + assert map_region("Montréal, QC")[0] == "Montréal" | |
| 60 | + assert map_region("Blainville, Québec")[0] == "Laurentides" | |
| 61 | + assert map_region("Los Angeles (originaire du Québec)") == (None, None) | |
| 62 | + assert map_region(None) == (None, None) | |
| 63 | + | |
| 64 | + | |
| 65 | +def test_parse_count(): | |
| 66 | + assert parse_count("1,2 M") == 1_200_000 | |
| 67 | + assert parse_count("12.3k") == 12_300 | |
| 68 | + assert parse_count("4 567 abonnés") == 4_567 | |
| 69 | + assert parse_count("1,234,567") == 1_234_567 | |
| 70 | + assert parse_count(2600000) == 2_600_000 | |
| 71 | + assert parse_count(None) is None | |
| 72 | + # un handle n'est JAMAIS un compteur (bug réel : « @chandrayoga-tw5bu » → 5 G) | |
| 73 | + assert parse_count("@chandrayoga-tw5bu") is None | |
| 74 | + assert parse_count("@nom123") is None | |
| 75 | + | |
| 76 | + | |
| 77 | +def test_audience_tier(): | |
| 78 | + assert audience_tier(5_000) == "nano" | |
| 79 | + assert audience_tier(50_000) == "micro" | |
| 80 | + assert audience_tier(500_000) == "macro" | |
| 81 | + assert audience_tier(2_000_000) == "mega" | |
| 82 | + assert audience_tier(None) == "nano" | |
added
tests/test_schema.py
+50 −0
@@ -0,0 +1,50 @@ | ||
| 1 | +# ============================================================================== | |
| 2 | +# Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 3 | +# File: tests/test_schema.py | |
| 4 | +# Desc: Validation de schéma (§18) — rejet des comptes sans url/confidence, | |
| 5 | +# agrégats (tier, portée, plateforme principale) | |
| 6 | +# ============================================================================== | |
| 7 | +import pytest | |
| 8 | + | |
| 9 | +from creaka.identity import account | |
| 10 | +from creaka.schema import Creator, PlatformAccount | |
| 11 | + | |
| 12 | + | |
| 13 | +def test_compte_sans_confidence_rejete(): | |
| 14 | + with pytest.raises(ValueError): | |
| 15 | + PlatformAccount(platform="instagram", handle="nom", url="x", | |
| 16 | + confidence=None).finalize() | |
| 17 | + | |
| 18 | + | |
| 19 | +def test_compte_sans_url_canonisable_rejete(): | |
| 20 | + with pytest.raises(ValueError): | |
| 21 | + PlatformAccount(platform="site-web", handle="nom", url="", | |
| 22 | + confidence=0.9).finalize() | |
| 23 | + | |
| 24 | + | |
| 25 | +def test_creator_sans_compte_valide_rejete(): | |
| 26 | + with pytest.raises(ValueError): | |
| 27 | + Creator(source="t", external_id="", display_name="Nom", | |
| 28 | + platforms=[]).finalize() | |
| 29 | + | |
| 30 | + | |
| 31 | +def test_agregats(): | |
| 32 | + cr = Creator(source="t", external_id="", display_name="Nom Scène", platforms=[ | |
| 33 | + account("instagram", "nom", "listes_medias", followers=250_000), | |
| 34 | + account("tiktok", "nom", "listes_medias", followers=900_000), | |
| 35 | + ]).finalize() | |
| 36 | + assert cr.primary_platform == "tiktok" # plus grosse audience | |
| 37 | + assert cr.total_reach == 1_150_000 | |
| 38 | + assert cr.audience_tier == "macro" # basée sur la principale (900k) | |
| 39 | + assert cr.external_id == "nom-scene" | |
| 40 | + assert cr.languages == ["fr"] | |
| 41 | + | |
| 42 | + | |
| 43 | +def test_dedup_intra_fiche_meilleure_confiance(): | |
| 44 | + cr = Creator(source="t", external_id="", display_name="Nom", platforms=[ | |
| 45 | + account("instagram", "nom", "handle_match", followers=100), | |
| 46 | + account("instagram", "@NOM", "link_in_bio"), | |
| 47 | + ]).finalize() | |
| 48 | + assert len(cr.platforms) == 1 | |
| 49 | + assert cr.platforms[0].confidence == 0.95 | |
| 50 | + assert cr.platforms[0].followers == 100 | |
| 51 | ||