roamify / README.md
jofaichow's picture
v0.1.22 — Move cache files to .cache/ directory
d71458e
metadata
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 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

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

  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