A fast, filterable, mobile-friendly browser for every restaurant participating in Toronto's Winterlicious 2026 prix-fixe festival.
Live: https://asterling.github.io/WinterLicList/
The official Winterlicious site is slow, paginated, and hard to filter. This one loads every menu at once and lets you slice the list however you like:
- 🔎 Search restaurant names and dishes
- 🍱 Filter by cuisine, neighbourhood, lunch/dinner price
- 🌱 Veg / Vegan only toggle (only restaurants with qualifying items)
- ⭐ Michelin highlight
- ❤️ Favorites saved locally
- 🗺️ Map view powered by Leaflet + OpenStreetMap
- 🔗 Deep links — share any restaurant:
/#r=canoe - 📱 Mobile friendly
winterlic.py— Python scraper hitting Toronto's open-data endpoint. Auto-detects whether the active campaign is Winter- or Summerlicious based on the calendar (override withLICIOUS_SEASON=Winter|Summer).menus-latest.json— the freshest snapshot, always the file the site loads first.menus-{Winterlicious,Summerlicious}-YYYY.json— per-campaign archives.season.json—{season, year, label, fetched_at}; drives the page title.index.html/styles.css/app.js— static frontend, no build step, no framework.
Open index.html over http:// (not file://) so the JSON fetch succeeds. The quickest local server:
python3 -m http.server 8000
# then open http://localhost:8000python3 -m pip install requests
python3 winterlic.pyThis rewrites winterlicious_menus_2026.json from the city's live endpoint.
A GitHub Action runs this on a schedule — see .github/workflows/refresh-data.yml.
Each restaurant in the JSON can be augmented with four AI-generated fields:
ai.standouts— 3–5 menu items the LLM thinks are worth flagging, taken verbatim from the menuai.vibe_tags— 3–5 short tags from a controlled list ("date night", "casual", "upscale", "groups", "trendy", "cozy", "celebration", "hidden gem", "neighbourhood favourite", …)ai.one_liner— a single-sentence pitch that replaces boilerplate city descriptions on the card and modalai.dietary_summary— deterministic, not LLM: counts veg / vegan / GF items from the city's flags
The enrichment runs entirely locally via Ollama. No API keys, no per-query cost. Results are cached by content hash in enrichment-cache.json, so unchanged menus skip the LLM on subsequent runs.
-
Install Ollama — either the prebuilt installer from https://ollama.com/download, or build from source:
git clone --depth=1 https://github.com/ollama/ollama cd ollama # macOS users on Xcode 15+ may need: -ldflags="-extldflags=-Wl,-ld_classic" go build -o ollama .
-
Start the Ollama server (leave it running in another terminal):
ollama serve
-
Pull the default model (~4.7 GB on disk, ~5 GB RAM at runtime):
ollama pull qwen2.5:7b
Smaller alternatives if you're tight on RAM (lower quality):
qwen2.5:3b— ~2 GBqwen2.5:1.5b— ~1 GB (often hallucinates dish names; not recommended)
# Fetch + enrich in one shot
python3 winterlic.py --enrich
# Just enrich the JSON you already have
python3 winterlic.py --no-fetch --enrich
# Pick a different model
python3 winterlic.py --enrich --model qwen2.5:3b
# First N restaurants only (handy when iterating on prompts)
python3 winterlic.py --enrich --limit 10
# Force re-enrichment ignoring the cache
python3 enrich.py menus-latest.json --forceRuntimes on Apple Silicon for the full 240-restaurant sweep:
| Model | Steady-state | First-run total | Cached re-run |
|---|---|---|---|
qwen2.5:7b |
~5 s/restaurant | ~20 min | seconds |
qwen2.5:3b |
~3 s/restaurant | ~12 min | seconds |
qwen2.5:1.5b |
~3 s/restaurant | ~12 min | seconds |
The frontend reads ai.* fields if present and falls back gracefully if they're missing, so enrichment is always optional.
https://secure.toronto.ca/c3api_data/v2/DataAccess.svc/Licious/map_data — City of Toronto open data, no API key required.
MIT. Not affiliated with the City of Toronto or Winterlicious.
