Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: creaka/connectors/snowball.py4# Desc: Connecteur DÉCOUVERTE par boule de neige — exploite les profils5# reliés (`related_profiles`) captés par l'enrichissement Instagram6# (instagram-apify) pour créer de nouvelles fiches. FILTRE QC7# OBLIGATOIRE (marqueur québécois dans le nom/handle) : sans lui, la8# proximité algorithmique d'Instagram ramène les vedettes mondiales9# (constaté 2026-08-20 : cristiano, selenagomez, ferrari, nasa…).10# Aucune requête réseau (lit la base).11# ==============================================================================12"""Découverte par boule de neige sur les profils reliés Instagram.1314Déclaré `enrichment` pour recevoir la liste des créateurs existants, mais il15RETOURNE de NOUVELLES fiches (le pipeline sync_source les ajoute par uid). On16lit `account.metrics["related_profiles"]` (posé par instagram-apify), on écarte17les handles déjà connus, et on crée une graine par nouveau compte suggéré.1819GARDE-FOU QC (2026-08-20) : les suggestions d'Instagram débordent largement du20Québec (stars mondiales, marques, comptes US/BR) — 3 767 fiches hors-périmètre21ajoutées en un passage avant ce filtre. On n'émet donc QUE si le nom public ou22le handle porte un marqueur québécois (`is_quebec`). Périmètre (§1-2) : Créa-Ka23n'agrège que des créateurs QUÉBÉCOIS — mieux vaut manquer un profil QC sans24marqueur que d'inonder l'annuaire de vedettes américaines (§3 travail de25moine). Signal `mention` (0.60) : à confirmer par l'enrichissement (§12.1).26"""27from __future__ import annotations2829import re3031from ..identity import account32from ..normalize import canonical_url, strip_accents33from ..schema import Creator34from .base import BaseConnector35from .youtube_recherche import is_quebec3637# Comptes QC qui ne sont PAS des créateurs (revue manuelle du 2026-08-20 :38# 95 des 101 survivantes du filtre QC étaient des villes, offices de tourisme,39# ministères, partis, commerces, festivals, médias) — on les bloque à la40# source pour ne pas re-polluer l'annuaire à chaque vague.41_NON_CREATEUR = re.compile(42 r"ville|tourisme|musee|festival|fondation|ymca|universite|uqam|"43 r"umontreal|ulaval|cegep|bibliothe|librairie|boutique|clinique|"44 r"auberge|hotel|ministere|gouv|transport|elections|loto|\bparti\b|"45 r"liberal|solidaire|conservateur|justepourrire|telequebec|magazine|"46 r"societe|association|federation")474849def _est_organisation(text: str) -> bool:50 return bool(_NON_CREATEUR.search(strip_accents(text or "").lower()))515253class SnowballConnector(BaseConnector):54 source_id = "snowball-ig"55 kind = "enrichment"56 max_new = 4000 # garde-fou par vague5758 def enrich(self, creators: list[Creator]) -> list[Creator]:59 known: set[str] = set()60 for cr in creators:61 for a in cr.platforms:62 if a.platform == "instagram":63 known.add(a.handle)64 new: list[Creator] = []65 emitted: set[str] = set()66 for cr in creators:67 ig = next((a for a in cr.platforms68 if a.platform == "instagram"), None)69 if ig is None:70 continue71 for rel in (ig.metrics or {}).get("related_profiles", []) or []:72 handle = (rel.get("username") if isinstance(rel, dict)73 else rel)74 handle = (handle or "").strip().lstrip("@").lower()75 if (not handle or rel and isinstance(rel, dict)76 and rel.get("is_private")):77 continue78 if handle in known or handle in emitted:79 continue80 name = (rel.get("full_name") if isinstance(rel, dict)81 else None) or ""82 # périmètre QC obligatoire : marqueur québécois dans le nom83 # public ou le handle, sinon on n'émet PAS (§1-2) ; et on84 # écarte les organismes/marques/médias QC (pas des créateurs)85 if not is_quebec(f"{name} {handle}"):86 continue87 if _est_organisation(f"{name} {handle}"):88 continue89 emitted.add(handle)90 try:91 acc = account("instagram", handle, "mention",92 url=canonical_url("instagram", handle),93 verified=(rel.get("is_verified")94 if isinstance(rel, dict)95 else None)).finalize()96 except Exception:97 continue98 new.append(Creator(99 source=self.source_id,100 external_id=f"instagram:{handle}",101 display_name=name or handle,102 platforms=[acc],103 notes="découvert par boule de neige (profils reliés IG)",104 ))105 if len(new) >= self.max_new:106 return new107 return new108