BirdSignal is a full-stack course discovery app that identifies likely "bird courses" by analyzing Reddit discussions and combining NLP/sentiment signals with a curated course catalog.
- Fetches posts from
r/wlurelated to bird courses. - Extracts course mentions (for example,
BU111,EM203) from thread text. - Computes per-course bird scores and supporting signals.
- Generates per-course JSON artifacts consumed by the frontend.
- Displays ranked courses with filtering, confidence signals, and evidence-ranked threads.
birdcourse/
├── frontend/ # React + TypeScript + Vite UI
│ ├── src/ # UI components and client-side enrichment logic
│ ├── public/course_details/ # Generated course JSON files used by UI
│ └── scripts/ # Catalog sync/normalize scripts
├── backend/
│ ├── reddit_api/ # Node.js API for Reddit fetching and lookup endpoints
│ └── data/ # Python pipeline + sentiment analysis
└── scripts/
└── update-term.sh # One-command refresh: sync + pipeline + build
backend/reddit_apiserves endpoints that fetch Reddit search results and expose course-thread/top-course data.backend/data/pipeline.pycalls the API, runs sentiment analysis, and writes processed outputs.- Generated course files are copied into
frontend/public/course_details/. frontend/src/App.tsxloads those JSON files and enriches display data (confidence + evidence ranking).
- Frontend: React 19, TypeScript, Vite, Tailwind CSS 4, Vaul (mobile drawer)
- Backend API: Node.js, Express, CORS middleware
- Data pipeline: Python 3.11+, NLTK, VADER sentiment, regex-based course extraction
- Tooling:
pnpm,uv, Bash automation
- Node.js 18+
pnpm- Python 3.11+
uv(recommended)curl
No Reddit account or credentials are required. By default, BirdSignal uses Reddit's public RSS search feed. Course detail refreshes use course-specific RSS searches, which are deliberately rate-limited and can take a while for a large catalog.
OAuth is optional and makes refreshes faster. To enable it, create a Reddit
application at https://www.reddit.com/prefs/apps (choose the script
application type), then copy the environment template:
cp .env.example .envFill in these values in .env:
REDDIT_CLIENT_ID=your_client_id
REDDIT_CLIENT_SECRET=your_client_secret
REDDIT_USER_AGENT=BirdSignal/1.0.0 by u/your_reddit_usernameKeep .env private. It is ignored by Git. If these values are absent, the
project automatically stays in account-free RSS mode. Your Reddit password is
never needed by this project.
cd backend/reddit_api
pnpm install
pnpm startAPI health check: http://localhost:3001/health
cd backend/data
uv sync
uv run python bootstrap_nltk.py
uv run python pipeline.py \
--api-url http://localhost:3001 \
--no-prompt \
--time-period all \
--limit 500 \
--analyze-top-courses \
--top-courses-count 80cd frontend
pnpm install
pnpm run devOpen Vite URL (usually http://localhost:5173).
Use this when you want fresh catalog + fresh analyzed course data + production build:
./scripts/update-term.shScript workflow:
- Ensures Reddit API is running.
- Syncs/normalizes curated course catalog.
- Runs Python pipeline.
- Copies processed course JSON into frontend public data.
- Builds frontend output.
Optional environment variables:
API_URL(defaulthttp://localhost:3001)TIME_PERIOD(defaultall)LIMIT(default500)TOP_COURSES_COUNT(default80)KEEP_API_RUNNING=1(do not kill spawned API process on exit)
Base URL: http://localhost:3001
Returns 200 OK when service is alive.
Query params:
limit(positive int, capped)timePeriod(hour|day|week|month|year|all)
Returns matching Reddit threads about bird courses.
Query params:
count(positive int)
Returns top courses from processed course detail artifacts.
Path params:
courseCode(validated course code format)
Query params:
limit(positive int)
Returns deduplicated course-specific threads (title/body/general search).
Set these in your shell or .env before starting backend/reddit_api:
REDDIT_CLIENT_ID(optional; enables faster OAuth requests)REDDIT_CLIENT_SECRET(optional; enables faster OAuth requests)REDDIT_USER_AGENT(optional; required only with OAuth)PORT(default3001)CORS_ORIGIN(comma-separated allowlist; empty means allow all origins)RATE_LIMIT_WINDOW_SECONDS(default60)RATE_LIMIT_MAX(default120)
cd backend/data
uv sync
uv run python bootstrap_nltk.py
uv run python pipeline.py
uv run python pipeline_smoke_test.pyUseful options for pipeline.py:
--api-url(defaulthttp://localhost:3001)--limit(default200)--time-period(hour|day|week|month|year|all)--analyze-top-courses--skip-top-courses--top-courses-count(default15)--no-prompt
cd frontend
pnpm install
pnpm run dev
pnpm run build
pnpm run lintCatalog scripts:
# Normalize existing raw catalog JSON
pnpm run catalog:normalize
# Fetch from Google Sheet, normalize, and merge with existing catalog
pnpm run catalog:syncIf needed, override the sheet URL:
BIRDCOURSE_SHEET_URL="<google-sheet-url>" pnpm run catalog:syncPipeline output files are written under backend/data/processed/ and then copied to frontend public assets.
Refreshes merge newly fetched posts with the last successful course output, and
the update script refuses to publish a catalog whose total post count dropped.
For an intentional reset, run with ALLOW_POST_COUNT_DROP=1.
Primary files used by UI:
frontend/public/course_details/catalog.jsonfrontend/public/course_details/index.jsonfrontend/public/course_details/<COURSE_CODE>.json
Curated catalog files:
frontend/public/data/course-catalog/raw.jsonfrontend/public/data/course-catalog/normalized.jsonfrontend/public/data/course-catalog/by-code.json
Bird score and ranking are heuristic, not an objective measure of course quality.
- Python pipeline computes base bird scores using sentiment, bird-term detection, mention counts, engagement signals, and department/level adjustments.
- Frontend computes display confidence (
high|medium|low) from mention volume and recency. - Low-confidence courses intentionally hide the numeric bird score and surface warning context.
- Threads inside course details are evidence-ranked to prioritize stronger signals.
Run:
cd backend/data
uv run python bootstrap_nltk.py- Ensure API is running on expected URL (
http://localhost:3001by default). - Verify
GET /healthresponds with200. - Retry with a broader time period (
--time-period all).
- Confirm generated files exist in
frontend/public/course_details/. - Re-run pipeline and copy step (or run
./scripts/update-term.sh).
- Reddit source is currently scoped to
r/wlusearch. - Scores are heuristic and sensitive to sample size and recency.
- Catalog/category quality depends on source sheet quality and normalization.
No license file is currently included in this repository.