roamify / README.md
jofaichow's picture
v0.1.22 — Move cache files to .cache/ directory
d71458e
---
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
```bash
# 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 `certifi` CA bundle. If the Python OpenAI client times out against ollama.com, run `pip 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
```bash
# ── 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:
```bash
# 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:
```bash
# 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
1. Pre-warm caches locally: `python scripts/warmup.py`
2. Pre-warm translations: `python scripts/prewarm_translations.py --lang Japanese --lang "Traditional Chinese"`
3. Run health check: `python scripts/check_cache.py`
4. Push everything (including cache files) to your HF Space
5. 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