| --- |
| 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 |
|
|