title: Roamify
emoji: ✈️
colorFrom: gray
colorTo: blue
sdk: docker
app_port: 8501
tags:
- streamlit
pinned: false
short_description: AI Travel Planner
license: mit
Roamify
AI-powered travel planner. Pick a city, choose a category, get personalized recommendations with photos, a map, and optional translations.
Built with Streamlit, powered by Hermes Agent.
Quick Start
# 1. Clone and enter the project
git clone <repo-url> roamify
cd roamify
# 2. Create a .env file (copy the template below)
# At minimum: OPENROUTER_API_KEY is needed for the primary provider
# 3. Install dependencies
pip install -r requirements.txt
# 4. Run the app
streamlit run src/streamlit_app.py --server.port 12345
API Keys & Provider Chain
The app uses a fallback chain of LLM providers. It tries each in order until one returns valid results:
| Priority | Provider | Model | Env Var | Required? |
|---|---|---|---|---|
| 1 (primary) | OpenRouter | deepseek/deepseek-v4-flash:free |
OPENROUTER_API_KEY |
✅ Highly recommended |
| 2 (fallback) | Ollama Cloud | deepseek-v4-flash:cloud |
OLLAMA_API_KEY |
Optional |
| 3 (fallback) | OpenRouter (Gemma) | google/gemma-4-26b-a4b-it:free |
(uses same OPENROUTER_API_KEY) |
Optional |
| 4 (last resort) | Gemini | gemini-2.5-flash |
GEMINI_API_KEY |
Optional (free quota may be exhausted) |
Note: Ollama Cloud requires an up-to-date
certifiCA bundle. If the Python OpenAI client times out against ollama.com, runpip install --upgrade certifi.
All providers use OpenAI-compatible API endpoints. Temperature is configurable:
- Search → temperature=0 (deterministic, cached results)
- Surprise Me → temperature=0.7 (creative, bypasses cache)
.env template
# ── Provider 1: OpenRouter (primary) ──
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
OPENROUTER_MODEL=deepseek/deepseek-v4-flash:free
# ── Provider 2: Ollama Cloud (fallback) ──
OLLAMA_API_KEY=ollama-...
OLLAMA_BASE_URL=https://ollama.com/v1
OLLAMA_MODEL=deepseek-v4-flash:cloud
# ── Provider 3: Gemma 4 on OpenRouter (second fallback) ──
# Uses the same OPENROUTER_API_KEY as Provider 1
# ── Provider 4: Gemini (last resort) ──
GEMINI_API_KEY=AIza...
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
GEMINI_MODEL=gemini-2.5-flash
# ── Unsplash for image enrichment (optional) ──
UNSPLASH_ACCESS_KEY=your-key-here
A provider is skipped if its API key is empty. Just set OPENROUTER_API_KEY and the rest will fall back automatically.
Features
- 61 cities across Asia, Europe, Africa, Americas & Oceania
- 7 travel categories: Landmark, Culture, Nature, Gems, Photo, Food, Shopping
- AI-generated recommendations with descriptions, tips, and coordinates
- 5-tier image fallback + emoji: Wikipedia → Wikidata → Commons → Local name → Unsplash → emoji (🏛️)
- Real coordinates from Nominatim geocoding with LLM-coord fast-path
- Leaflet map with spider markers, card↔map hover sync
- Multi-language translation: Traditional Chinese, Japanese, Korean, French, Spanish, German
- Japanese & Traditional Chinese pre-warmed — 61 cities × 7 categories translated upfront
- Disk-persisted caches — repeat searches are instant, survive restarts
- Deterministic mode (Search) vs Creative mode (Surprise Me button)
- Dark Cyborg theme with large fonts
- Responsive 4-row stacking — search controls auto-stack into rows when viewport is narrower than 50% of screen width, content-aware JS detects exact wrap point
Caches
Four JSON cache files are committed and ship with the app:
| Cache | Key | What it stores |
|---|---|---|
.cache/llm_cache.json |
(city, categories_hash) |
Full recommendation data (num-agnostic — 3, 6, 9, 12, 15 all hit same cache) |
.cache/image_cache.json |
(attraction_name, city, country) |
Image URLs from all 6 tiers |
.cache/geocode_cache.json |
Nominatim query string | Lat/lon + bounding box |
.cache/translation_cache.json |
(city, categories_hash, language) |
Translated descriptions, tips, and names |
Caches are populated on first search and persisted to disk. On HF Spaces, they survive restarts and provide instant results for cached cities.
Warmup
LLM data can be pre-generated offline so the app is fast from the first load:
# Full warmup (LLM + image enrichment)
python scripts/warmup.py
# Prewarm remaining uncached cities (concurrent 2-worker)
python scripts/prewarm_remaining.py
# Warmup specific cities
python scripts/warmup.py -c "Hong Kong" -c Singapore
# Fix-only mode: re-check images on cached entries
python scripts/warmup.py --fix
Generates up to 427 city × category combos (8,100+ items across 4 caches). Resumable — interrupted runs pick up where they left off.
scripts/prewarm_remaining.py targets remaining uncached cities — useful
for expanding coverage after the initial warmup.
Translation Pre-Warm
Translates all cached city+category combos into target languages. Skips entries already in the translation cache for quick resume:
# Pre-warm Japanese + Traditional Chinese (our two primary languages)
python scripts/prewarm_translations.py --lang Japanese --lang "Traditional Chinese"
# Force re-translate everything
python scripts/prewarm_translations.py --lang Japanese --force
# Add more languages
python scripts/prewarm_translations.py --lang Korean --lang French
~426 LLM cache entries × 2 languages = ~852 translation calls. Each translates all 19 items in a single LLM call. Takes ~2-4 hours to complete.
Project Structure
roamify/
├── src/
│ ├── streamlit_app.py # Main Streamlit app
│ ├── services/
│ │ └── recommender.py # LLM calls, geocoding, images, caching
│ ├── styles/
│ │ └── dark_theme.py # Dark CSS + JS (hover sync, flex panels)
│ └── utils/
│ └── prompts.py # Category-specific AI prompt templates
├── scripts/
│ ├── warmup.py # Full 28-city unified warmup (LLM + images + geocode)
│ ├── prewarm_translations.py # Translation pre-warm (JA, TC, etc.)
│ ├── prewarm_remaining.py # Prewarm remaining uncached cities
│ ├── prewarm_12_remaining.py # Targeted prewarm for specific city list
│ ├── prewarm_retry_missing.py # Single-threaded retry for missing combos
│ ├── check_cache.py # Cache health check & repair
│ ├── fix_images.py # Parallel image enrichment pass
│ └── clear_poor_entries.py # Clear cache for re-warmup
├── .streamlit/
│ └── config.toml # Streamlit server and theme config
├── .cache/
│ ├── llm_cache.json # Disk-persisted recommendation cache (~3.3MB)
│ ├── image_cache.json # Disk-persisted image URL cache (~1.0MB)
│ ├── geocode_cache.json # Disk-persisted geocoding cache (~560KB)
│ └── translation_cache.json # Disk-persisted translation cache (~7.3MB)
├── Dockerfile # HF Spaces deployment
├── requirements.txt
└── README.md
Deploying to HF Spaces
- Pre-warm caches locally:
python scripts/warmup.py - Pre-warm translations:
python scripts/prewarm_translations.py --lang Japanese --lang "Traditional Chinese" - Run health check:
python scripts/check_cache.py - Push everything (including cache files) to your HF Space
- Set secrets in HF Space Settings (same keys as your
.env)
Large cache files are normal — they're JSON and compress well in git.
.cache/llm_cache.json is typically 3.3MB, translation cache ~7.3MB,
images cache is URL-only (1.0MB).
License
MIT