Niches¶
The pipeline is niche-agnostic: it scrapes creators in one niche, scores every post for
virality, and turns the winners into blueprints. Which niche it targets is defined by a single
human-editable file per niche, in the repo's niches/ directory:
niches/
fashion.yaml # the canonical example — Fashion is the default on `main`
football.yaml
cricket.yaml
travel.yaml
Each *.yaml is the one source of truth for a niche. scripts/new-niche.sh reads it and
writes it into the pipeline's real per-platform config on a dedicated git branch — so every
niche becomes "the same full pipeline, in its own branch."
Fashion is the default
main ships with Fashion as the worked example. Other niches are applied onto their own
niche/<niche> branch, leaving main untouched.
The *.yaml schema¶
A niche file has a display niche name and a platforms block for the three fixed platforms
(instagram, x, youtube). Everything inside a platform block is niche-tunable:
niche: Fashion # display name, written into every niche_config.json
platforms:
instagram:
reels_per_creator: 250 # per-creator scrape limit
# (x: posts_per_creator, youtube: shorts_per_creator)
discovery: # instagram-only keyword/seed discovery (OFF by default)
keywords: [fashion, ootd, ...] # relevance keywords for auto-discovery
seeds: [] # seed profiles to expand from
weights: # the 4 virality signals (auto-normalized to sum to 1)
reach_multiplier: 0.35 # plays / followers
outlier_score: 0.25 # plays / that creator's median plays
engagement_rate: 0.25 # (likes+comments+shares+saves) / followers
velocity: 0.15 # plays / days since posting
tiers: # score → label buckets (0-100)
- { label: Viral, min_score: 85 }
- { label: High, min_score: 70 }
- { label: Above Average, min_score: 50 }
- { label: Normal, min_score: 0 }
seed_pages: # EXAMPLE handles → starter pages.txt (replace them!)
- "@example.fashion.brand"
x: { ... same knobs ... }
youtube:{ ... same knobs ... }
Niche-specific vs fixed fields¶
The script only swaps the niche-specific values into each platform's
ReelScraper/platforms/<p>/niche_config.json; the file's structure and comment fields are
preserved.
| Niche-specific (set from the YAML) | Fixed / preserved (untouched by the script) |
|---|---|
niche display name |
_comment_discovery, _comment_virality, _note comment fields |
reels_/posts_/shorts_per_creator limit |
discovery.enabled + mechanics (search_terms, per_query, …) |
discovery.keywords, discovery.seeds (IG only) |
virality.top_n |
virality.weights, virality.tiers |
the JSON key order / overall shape |
Tuning weights per niche is the point. Sports clips (football, cricket) are velocity-driven and spike fast, so velocity + reach are up-weighted; travel is slow-burn and evergreen, so velocity is down-weighted and engagement/outlier carry more of the score.
Handle rules for seed_pages
So the scrapers accept them: Instagram allows dots (@example.fashion.brand); X allows
no dots and max 15 chars (@ex_fashion_1); YouTube allows dots and hyphens
(@example.style.tv). All shipped example handles are obviously fake — replace them with
real creators.
How new-niche.sh works¶
./scripts/new-niche.sh <niche> # <niche> must match niches/<niche>.yaml
- Validates
niches/<niche>.yamlexists (lists available niches if not). - Creates + switches to a new git branch
niche/<niche>— and refuses to clobber an existing branch of that name (re-running while already on it re-applies in place). - Writes each
niche_config.jsonfrom the YAML, overlaying only niche-specific values onto the existing file (comments/structure preserved). Usespython3+ PyYAML, falling back touv runifpython3lacks PyYAML. - Writes each
pages.txtfromseed_pages, with a header explaining these are examples. - Stages the changes (
git add) and prints next steps. It does not commit or push.
The result: Fashion stays the default on main; each other niche lives as a full,
ready-to-run pipeline on its own niche/<niche> branch.
Add a brand-new niche¶
cp niches/fashion.yaml niches/skincare.yaml
# edit niches/skincare.yaml — set `niche:`, swap keywords, tune weights, add example seed_pages
./scripts/new-niche.sh skincare
Worked example¶
./scripts/new-niche.sh cricket
produces, on a new niche/cricket branch:
ReelScraper/platforms/instagram/niche_config.json niche="Cricket", cricket keywords, sport-tuned weights
ReelScraper/platforms/x/niche_config.json niche="Cricket", velocity-led weights
ReelScraper/platforms/youtube/niche_config.json niche="Cricket", reach/outlier-led weights
ReelScraper/platforms/<p>/pages.txt example @handles to replace with real creators
Then, per the printed next steps:
# 1. put REAL creator handles in each platforms/<p>/pages.txt
# 2. set up secrets: cp .env.example .env (X needs a burner session)
cd ReelScraper
uv run cli.py scrape instagram
uv run cli.py analyze instagram
uv run cli.py media instagram
uv run cli.py start # prints HUB_URL=http://127.0.0.1:<port> (8787 if free)
Running two niches at once¶
One clone per niche. new-niche.sh branches a whole pipeline, so the clean way to work on
Fashion and Fitness at the same time is two checkouts, each on its own branch:
git clone git@github.com:VincitoreSi/TheCuttingRoom.git fashion
git clone git@github.com:VincitoreSi/TheCuttingRoom.git fitness
cd fitness && ./scripts/new-niche.sh fitness && ./init
They cannot touch each other. Nothing in this project writes outside its own directory —
no shared cache, no file in $HOME — so the only thing two clones share is the loopback
network. Two things follow from that, and ./init handles both:
Each checkout owns a port. The first clone takes 8787, the second 8788, and the choice
is pinned in that clone's ReelScraper/.env as HUB_PORT. It is a pin, not a race: the
port stays the same across restarts, so it can be bookmarked and pointed at. Set HUB_PORT
by hand (or ./init --port 8790) to choose your own.
Each checkout's agents dial their own hub. ./init writes BACKEND_API into every
component's .env. This is the one that matters: cd SimilarContent && uv run cli.py
propose resolves the hub from that file, and a .env copied between clones — or a stale
export BACKEND_API= in your shell — would post this niche's proposals into the other
niche's studio, against the other niche's corpus, with every call returning 200.
So the agents check. Each one asks GET /api/hub at startup and refuses to run if the hub
belongs to a different checkout:
ERROR: http://127.0.0.1:8788 is a different checkout's hub.
it serves: /Users/you/fitness/ReelScraper
this agent: /Users/you/fashion/AnalysisEngine
Telling the two boards apart
Two Dashboards look identical. The sidebar footer carries the niche name from
niche_config.json above the host and port, which is the quickest way to know which
one you are looking at.
./stop and ./clean are scoped the same way — they match processes by working directory,
never by command line, so stopping one clone never touches the other's hub.