SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
5.1 KB · 108 lines python
Raw Blame History
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