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