Skill: mehediahamed/travel-destination-brochure (version 1.0.0)

--- name: travel-destination-brochure description: "Build travel destination scenarios and brochures from a city name. Fetches street-level and landmark imagery from OpenStreetCam and Wikimedia Commons, then uses VLM Run (vlmrun) to generate a travel video and a travel plan. Use when the user wants a travel brochure, destination guide, travel video, or travel planning for a city." --- # Travel Destination Brochure & Video Create travel brochures, videos, and 1-day plans for a destination city by combining OpenStreetCam street-level photos, Wikimedia Commons imagery, and VLM Run for video and copy. ## Prerequisites Before starting, ensure you have: - **Python 3.10 or higher** installed - **Internet connection** (for downloading images and API access) - **VLMRUN_API_KEY** (optional, but required for video and travel plan generation) **No API keys required for:** - OpenStreetCam (public read access) - Wikimedia Commons (public access) - Nominatim geocoding (public access) ## Installation Steps ### Step 1: Verify Python Installation Check if Python 3.10+ is installed: **Windows (PowerShell):** ```powershell python --version # Should show Python 3.10.x or higher ``` **macOS/Linux:** ```bash python3 --version # Should show Python 3.10.x or higher ``` If Python is not installed or is an older version: - **Windows**: Download from [python.org](https://www.python.org/downloads/) - **macOS**: `brew install python@3.11` (or use python.org installer) - **Linux**: `sudo apt install python3.11` (Ubuntu/Debian) or use your distribution's package manager ### Step 2: Install uv (Package Manager) **Windows (PowerShell):** ```powershell # Using pip pip install uv # Or using PowerShell installer powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` **macOS/Linux:** ```bash # Using pip pip install uv # Or using curl installer curl -LsSf https://astral.sh/uv/install.sh | sh ``` **Verify installation:** ```bash uv --version ``` ### Step 3: Create Virtual Environment Navigate to the skill directory and create a virtual environment: **Windows (PowerShell):** ```powershell cd c:\Users\mehed\.claude\skills\travel-destination-brochure uv venv .venv\Scripts\Activate.ps1 ``` **macOS/Linux:** ```bash cd ~/.claude/skills/travel-destination-brochure uv venv source .venv/bin/activate ``` **Note:** You should see `(.venv)` in your terminal prompt when activated. ### Step 4: Install Dependencies Install the required packages: ```bash # Install vlmrun CLI (required for video and travel plan generation) uv pip install "vlmrun[cli]" # Install requests (required for API calls) uv pip install requests ``` **Verify installation:** ```bash vlmrun --version python -c "import requests; print(requests.__version__)" ``` ### Step 5: Set Up VLMRUN_API_KEY (Optional but Recommended) To generate travel videos and plans, you need a VLMRUN API key: **Windows (PowerShell):** ```powershell # Set for current session Check .env file for api key $env:VLMRUN_API_KEY="your-api-key-here" # Set permanently (User-level) [System.Environment]::SetEnvironmentVariable('VLMRUN_API_KEY', 'your-api-key-here', 'User') ``` **macOS/Linux:** ```bash # Set for current session export VLMRUN_API_KEY="your-api-key-here" # Set permanently (add to ~/.bashrc or ~/.zshrc) echo 'export VLMRUN_API_KEY="your-api-key-here"' >> ~/.bashrc source ~/.bashrc ``` **Verify environment variable:** Read **.env** file to find api keys ```bash # Windows PowerShell echo $env:VLMRUN_API_KEY # macOS/Linux echo $VLMRUN_API_KEY ``` ### Step 6: Verify Installation Test that everything works: ```bash # Test geocoding (should work without API key) uv run scripts/geocode_city.py "Paris, France" # Test vlmrun (if API key is set) vlmrun --help ``` **Installation Complete!** You're ready to generate travel brochures. ## Quick Start (Recommended) Use the simplified all-in-one script that handles everything automatically: **Windows (PowerShell):** ```powershell uv run scripts/simple_travel_brochure.py --city "Doha, Qatar" ``` **macOS/Linux:** ```bash uv run scripts/simple_travel_brochure.py --city "Doha, Qatar" ``` **Alternative (if uv is not available):** ```bash python scripts/simple_travel_brochure.py --city "Doha, Qatar" ``` This script will: 1. Geocode the city name to coordinates 2. Fetch 3 street-level photos from OpenStreetCam 3. Fetch 2 landmark images from Wikimedia Commons (total 5 images) 4. Generate a 30-second travel video using vlmrun (if `VLMRUN_API_KEY` is set) 5. Generate a one-day travel plan using vlmrun (if `VLMRUN_API_KEY` is set) 6. Clean up temporary files automatically **Options:** - `--output DIR` — Output directory (default: `./travel_brochure`) - `--osc-count N` — Number of OpenStreetCam photos (default: 3) - `--commons-count N` — Number of Commons images (default: 2) **Note:** Set the `VLMRUN_API_KEY` environment variable to enable video and travel plan generation. The script will skip video generation gracefully if the API key is not set. **Example:** ```bash uv run scripts/simple_travel_brochure.py --city "Paris, France" --output ./paris_trip ``` **Output:** - `images/` — Downloaded photos (5 images total) - `manifest.json` — Metadata about the city, coordinates, and image paths - `video/` — Generated travel video (if VLMRUN_API_KEY is set) - `travel_plan.md` — One-day travel itinerary (if VLMRUN_API_KEY is set) --- ## Advanced: Step-by-Step Workflow For more control over each step, use the individual scripts below. ### Workflow Overview 1. **Collect input** – Get destination city from the user. 2. **Geocode** – Resolve city name to coordinates (lat, lng). 3. **Fetch imagery & info** – OpenStreetCam (nearby photos) + Wikimedia Commons (search images and metadata). 4. **Generate assets** – Use **vlmrun** to create a short travel video and a travel plan from the collected images and info. All paths below are relative to the directory containing this SKILL.md. **Run scripts using:** - `uv run scripts/script_name.py` (recommended - handles dependencies automatically via PEP 723) - `python scripts/script_name.py` (if dependencies are already installed) ### Step 1: Get Destination City Ask the user: *"Which city do you want the travel brochure and video for?"* Use the exact city name (and country/region if ambiguous) for geocoding and Commons search. ### Step 2: Geocode City Resolve city name to latitude/longitude (e.g. for OpenStreetCam and optional Commons geo-search). ```bash uv run scripts/geocode_city.py "Paris, France" # Or: python scripts/geocode_city.py "Tokyo" ``` Output: JSON with `lat`, `lng`, `display_name`. Use these in Steps 3–4. ### Step 3: Fetch OpenStreetCam Photos OpenStreetCam provides street-level imagery. Base URL: `https://api.openstreetcam.org/`. - **Nearby sequences:** `POST /nearby-tracks` — body: `lat`, `lng`, `distance` (km). - **Nearby photos:** `POST /1.0/list/nearby-photos/` — body: `lat`, `lng`, `radius` (meters), optional `page`, `ipp`. No `access_token` required for these read endpoints. Use `scripts/fetch_openstreetcam.py` to request photos and optionally download thumbnails/full images into a folder. ```bash uv run scripts/fetch_openstreetcam.py --lat 48.8566 --lng 2.3522 --radius 2000 --output ./assets/osc --max-photos 20 ``` Produces: image files under `--output` and a small manifest (e.g. `osc_manifest.json`) with captions/locations if available. ### Step 4: Fetch Wikimedia Commons Images & Info Commons provides landmark and cultural images. API: `https://commons.wikimedia.org/w/api.php`. - **Search:** `action=query`, `list=search`, `srsearch=<city or landmark>`, `srnamespace=6` (File namespace). - **Image URLs and metadata:** `action=query`, `prop=imageinfo`, `iiprop=url|extmetadata`, `titles=File:...`. Use `scripts/fetch_commons.py` to search by destination name, resolve file URLs, and optionally download to a folder. ```bash uv run scripts/fetch_commons.py --query "Paris landmarks" --output ./assets/commons --max-images 15 ``` Produces: image files and a manifest (e.g. `commons_manifest.json`) with captions/descriptions from Commons. ### Step 5: Aggregate Manifest for vlmrun Combine OSC and Commons manifests (and optionally add short text lines per image) into a single manifest or list that you can pass to vlmrun (e.g. paths + one short caption per image). The pipeline script can do this. ```bash uv run scripts/run_travel_pipeline.py --city "Paris, France" --output-dir ./travel_output ``` This script should: geocode → fetch OSC → fetch Commons → write `images/` and `manifest.json` (or `manifest.txt`) under `--output-dir`. ### Step 6: Generate Video and Travel Plan with vlmrun Use the **vlmrun-cli-skill** workflow: ensure `vlmrun` is installed and `VLMRUN_API_KEY` is set. **Travel video** – Pass the collected images and a single prompt so the model produces a short travel video (e.g. 30 seconds). Prefer `-o` to save the artifact. **Note:** If `VLMRUN_API_KEY` is set as an environment variable, you can omit `--api-key`: ```bash # Using environment variable (recommended) vlmrun chat "Create a 30-second travel video showcasing these images of [CITY]. Add subtle captions with the location names. Keep a calm, inspiring travel-documentary style." -i ./travel_output/images/photo1.jpg -i ./travel_output/images/photo2.jpg -i ./travel_output/images/photo3.jpg ... -o ./travel_output/video # Or using --api-key from .env, flag directly vlmrun --api-key "your-api-key-here" chat "Create a 30-second travel video showcasing these images of [CITY]. Add subtle captions with the location names. Keep a calm, inspiring travel-documentary style." -i ./travel_output/images/photo1.jpg -i ./travel_output/images/photo2.jpg -i ./travel_output/images/photo3.jpg ... -o ./travel_output/video ``` If the number of files is large, reference the manifest and pass a subset (e.g. up to 10–15 representative images) or use a prompt that says “using the attached images in order.” **Travel plan (1-day)** – Use the same images plus a text prompt to get a narrative or bullet-point plan. ```bash # Using environment variable (recommended) vlmrun chat "Using these images and their locations, write a one-day travel plan for [CITY]: morning, midday, and evening activities with specific places and practical tips. Output as structured markdown (headings and bullet points)." -i ./travel_output/images/photo1.jpg -i ./travel_output/images/photo2.jpg ... -o ./travel_output # Or using --api-key flag directly vlmrun --api-key "your-api-key-here" chat "Using these images and their locations, write a one-day travel plan for [CITY]: morning, midday, and evening activities with specific places and practical tips. Output as structured markdown (headings and bullet points)." -i ./travel_output/images/photo1.jpg -i ./travel_output/images/photo2.jpg ... -o ./travel_output ``` Save the model’s text response (and any artifact) under `--output-dir` (e.g. `travel_plan.md`). ## Scripts Reference | Script | Purpose | |--------|--------| | `scripts/geocode_city.py` | City name → lat, lng (Nominatim) | | `scripts/fetch_openstreetcam.py` | Fetch/download OpenStreetCam photos by lat/lng/radius | | `scripts/fetch_commons.py` | Search and download Wikimedia Commons images by query | | `scripts/run_travel_pipeline.py` | Run geocode + OSC + Commons and write manifest + images | ## API References - **OpenStreetCam:** [API Reference](https://api.openstreetcam.org/api/doc.html) — nearby-tracks, list/nearby-photos, auth only for uploads. - **Wikimedia Commons:** [Commons API](https://commons.wikimedia.org/w/api.php) — `action=query`, `list=search`, `prop=imageinfo`; [MediaWiki API help](https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page). - **vlmrun:** Use the **vlmrun-cli-skill** for setup, env vars, and all `vlmrun chat` options. ## Checklist for a Complete Run - [ ] User provided destination city (and country if needed). - [ ] Geocoded city and confirmed lat/lng. - [ ] Fetched OpenStreetCam photos; saved images and manifest. - [ ] Fetched Commons images for the destination; saved images and manifest. - [ ] Built aggregated manifest/images under one output dir. - [ ] Ran vlmrun with collected images to generate travel video; saved artifact with `-o`. - [ ] Ran vlmrun with same (or subset) images to generate travel plan; saved text as markdown. ## Troubleshooting ### Installation Issues **Python not found:** - **Windows**: Ensure Python is added to PATH during installation, or use `py` instead of `python` - **macOS/Linux**: Use `python3` instead of `python` **uv command not found:** - Restart your terminal after installation - **Windows**: Check if uv is in your PATH: `$env:PATH` - **macOS/Linux**: Ensure `~/.cargo/bin` or `~/.local/bin` is in your PATH **Virtual environment activation fails:** - **Windows PowerShell**: If you get an execution policy error, run: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` - **Windows CMD**: Use `.venv\Scripts\activate.bat` instead of `.ps1` - **macOS/Linux**: Ensure you're using `source .venv/bin/activate` (not `./.venv/bin/activate`) **vlmrun not found:** - Ensure virtual environment is activated - Reinstall: `uv pip install "vlmrun[cli]"` - Verify: `which vlmrun` (macOS/Linux) or `where.exe vlmrun` (Windows) ### Runtime Issues - **Geocode fails** – Try adding country/region, or use “City, Country” format. - **OpenStreetCam returns few/no results:** - Increase radius parameter (default: 2000m, try 5000m or 10000m) - Try city center coordinates instead of outskirts - Some regions have sparse coverage; try nearby major cities - **Commons returns few results** – Broaden query (e.g. “City tourism”, “City sights”). - **vlmrun errors:** - Confirm `VLMRUN_API_KEY` is set correctly: `echo $VLMRUN_API_KEY` (macOS/Linux) or `echo $env:VLMRUN_API_KEY` (Windows) - Check network connection - Reduce number of input images if hitting API limits (try 5-10 images instead of 20+) - Verify API key is valid and has sufficient credits/quota **Script execution errors:** - Ensure you're in the correct directory (skill root directory) - Check that virtual environment is activated - Verify all dependencies are installed: `uv pip list` ## Example End-to-End ```bash # 1) Ask user for city, then run pipeline (e.g. "Paris, France") uv run scripts/run_travel_pipeline.py --city "Paris, France" --output-dir ./travel_output # 2) Generate travel video (use image paths from travel_output/images/ or image_paths.txt) vlmrun chat "Create a 30-second travel video from these images of Paris. Add short location captions. Calm documentary style." -i ./travel_output/images/img_0000.jpg -i ./travel_output/images/img_0001.jpg -o ./travel_output/video # 3) Generate 1-day travel plan (same images) vlmrun chat "Using these photos of Paris, write a one-day travel plan (morning, midday, evening) with specific places and tips in markdown." -i ./travel_output/images/img_0000.jpg -i ./travel_output/images/img_0001.jpg -o ./travel_output ``` ## Quick Reference: Key URLs - OpenStreetCam API base: `https://api.openstreetcam.org/` - Commons API: `https://commons.wikimedia.org/w/api.php` - Nominatim geocoding: `https://nominatim.openstreetmap.org/search?q=<query>&format=json`

path: .env_template.txt
content: VLMRUN_API_KEY="your-api_key"
VLMRUN_BASE_URL="https://agent.vlm.run/v1"


path: README.md
content: # Travel Destination Brochure Generator

A Python-based tool that automatically generates travel brochures, videos, and one-day travel plans for any destination city. The tool combines street-level imagery from OpenStreetCam, landmark photos from Wikimedia Commons, and AI-powered video generation using VLM Run to create comprehensive travel content.

## 🌟 Features

- **Automatic City Geocoding**: Converts city names to coordinates using OpenStreetMap's Nominatim service
- **Street-Level Imagery**: Fetches real street photos from OpenStreetCam API
- **Landmark Photography**: Retrieves high-quality landmark images from Wikimedia Commons
- **AI Video Generation**: Creates 30-second travel videos using VLM Run's visual AI
- **Travel Planning**: Generates detailed one-day itineraries with morning, midday, and evening activities
- **All-in-One Script**: Simple command-line interface for quick brochure generation
- **Modular Design**: Individual scripts for custom workflows and advanced usage

## 📋 Prerequisites

Before you begin, ensure you have:

- **Python 3.10 or higher** installed
- **Internet connection** (required for API calls and image downloads)
- **VLMRUN_API_KEY** (optional, but required for video and travel plan generation)

**No API keys required for:**
- OpenStreetCam (public read access)
- Wikimedia Commons (public access)
- Nominatim geocoding (public access)

## 🚀 Quick Start

### Installation

1. **Verify Python Installation**

   ```powershell
   # Windows (PowerShell)
   python --version
   # Should show Python 3.10.x or higher
   ```

   ```bash
   # macOS/Linux
   python3 --version
   # Should show Python 3.10.x or higher
   ```

2. **Install uv Package Manager** (recommended)

   ```powershell
   # Windows (PowerShell)
   pip install uv
   # Or using installer
   powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
   ```

   ```bash
   # macOS/Linux
   pip install uv
   # Or using installer
   curl -LsSf https://astral.sh/uv/install.sh | sh
   ```

3. **Install Dependencies**

   ```bash
   # Install vlmrun CLI (required for video and travel plan generation)
   uv pip install "vlmrun[cli]"

   # Install requests (required for API calls)
   uv pip install requests
   ```

4. **Set Up VLMRUN_API_KEY** (Optional but Recommended)

   Create a `.env` file in the project root (see `.env_template` for reference):

   ```env
   VLMRUN_API_KEY="your-api-key-here"
   VLMRUN_BASE_URL="https://agent.vlm.run/v1"
   ```

   Or set it as an environment variable:

   ```powershell
   # Windows (PowerShell)
   $env:VLMRUN_API_KEY="your-api-key-here"
   ```

   ```bash
   # macOS/Linux
   export VLMRUN_API_KEY="your-api-key-here"
   ```

### Generate Your First Travel Brochure

Use the simplified all-in-one script:

```powershell
# Windows (PowerShell)
uv run scripts/simple_travel_brochure.py --city "Doha, Qatar"
```

```bash
# macOS/Linux
uv run scripts/simple_travel_brochure.py --city "Doha, Qatar"
```

**Alternative (if uv is not available):**
```bash
python scripts/simple_travel_brochure.py --city "Doha, Qatar"
```

This script will:
1. ✅ Geocode the city name to coordinates
2. ✅ Fetch 3 street-level photos from OpenStreetCam
3. ✅ Fetch 2 landmark images from Wikimedia Commons (total 5 images)
4. ✅ Generate a 30-second travel video using vlmrun (if `VLMRUN_API_KEY` is set)
5. ✅ Generate a one-day travel plan using vlmrun (if `VLMRUN_API_KEY` is set)
6. ✅ Save all results to `./travel_brochure/` directory

**Output Structure:**
```
travel_brochure/
├── images/
│   ├── image_00.jpg
│   ├── image_01.jpg
│   ├── image_02.jpg
│   ├── image_03.jpg
│   └── image_04.jpg
├── manifest.json          # Metadata about city, coordinates, and images
├── video/                 # Generated travel video (if API key is set)
└── travel_plan.md         # One-day travel itinerary (if API key is set)
```

## 📖 Detailed Usage

### Script Options

#### `simple_travel_brochure.py` (Recommended for Beginners)

The easiest way to generate a complete travel brochure:

```bash
uv run scripts/simple_travel_brochure.py --city "Paris, France" --output ./paris_trip --osc-count 3 --commons-count 2
```

**Options:**
- `--city` (required): City name, e.g., "Paris, France" or "Tokyo"
- `--output DIR`: Output directory (default: `./travel_brochure`)
- `--osc-count N`: Number of OpenStreetCam photos (default: 3)
- `--commons-count N`: Number of Commons images (default: 2)

#### `run_travel_pipeline.py` (Advanced Workflow)

For more control over the image collection process:

```bash
uv run scripts/run_travel_pipeline.py --city "Paris, France" --output-dir ./travel_output --max-photos 15 --max-commons 10
```

**Options:**
- `--city` (required): Destination city name
- `--output-dir DIR`: Output directory (default: `./travel_output`)
- `--radius METERS`: OpenStreetCam search radius in meters (default: 2000)
- `--max-photos N`: Max OpenStreetCam photos (default: 15)
- `--max-commons N`: Max Commons images (default: 10)
- `--skip-osc`: Skip OpenStreetCam fetch
- `--skip-commons`: Skip Wikimedia Commons fetch

**Output:**
- `images/`: All downloaded images (renamed as `img_0000.jpg`, `img_0001.jpg`, etc.)
- `manifest.json`: Combined manifest with all image metadata
- `image_paths.txt`: List of image paths for easy use with vlmrun

#### Individual Scripts

For step-by-step control, use these scripts individually:

**1. Geocode City**
```bash
uv run scripts/geocode_city.py "Paris, France"
# Output: JSON with lat, lng, display_name
```

**2. Fetch OpenStreetCam Photos**
```bash
uv run scripts/fetch_openstreetcam.py --lat 48.8566 --lng 2.3522 --radius 2000 --output ./assets/osc --max-photos 20
```

**3. Fetch Wikimedia Commons Images**
```bash
uv run scripts/fetch_commons.py --query "Paris landmarks" --output ./assets/commons --max-images 15
```

### Generating Videos and Travel Plans

After collecting images, use `vlmrun` to generate content:

**Generate Travel Video:**
```bash
vlmrun chat "Create a 30-second travel video showcasing these images of Paris. Add subtle captions with location names. Keep a calm, inspiring travel-documentary style." \
  -i ./travel_output/images/img_0000.jpg \
  -i ./travel_output/images/img_0001.jpg \
  -i ./travel_output/images/img_0002.jpg \
  -o ./travel_output/video
```

**Generate Travel Plan:**
```bash
vlmrun chat "Using these images and their locations, write a one-day travel plan for Paris: morning, midday, and evening activities with specific places and practical tips. Output as structured markdown." \
  -i ./travel_output/images/img_0000.jpg \
  -i ./travel_output/images/img_0001.jpg \
  -i ./travel_output/images/img_0002.jpg \
  -o ./travel_output
```

## 📁 Project Structure

```
travel-destination-brochure/
├── README.md                    # This file
├── SKILL.md                     # Skill documentation for AI agents
├── .env_template               # Template for environment variables
├── references/                  # API documentation references
│   ├── openstreetcam_api.md    # OpenStreetCam API reference
│   └── commons_api.md          # Wikimedia Commons API reference
└── scripts/                     # Python scripts
    ├── geocode_city.py          # City name → lat/lng (Nominatim)
    ├── fetch_openstreetcam.py   # Fetch OpenStreetCam photos by coordinates
    ├── fetch_commons.py         # Search and download Commons images
    ├── run_travel_pipeline.py   # Full pipeline: geocode + OSC + Commons
    └── simple_travel_brochure.py # All-in-one simplified script
```

## 🔧 Scripts Reference

| Script | Purpose | Key Features |
|--------|---------|--------------|
| `geocode_city.py` | Convert city name to coordinates | Uses Nominatim (OpenStreetMap), returns lat/lng/display_name |
| `fetch_openstreetcam.py` | Fetch street-level photos | Searches by lat/lng/radius, downloads thumbnails/full images |
| `fetch_commons.py` | Search and download Commons images | Searches by query, extracts metadata and captions |
| `run_travel_pipeline.py` | Complete image collection pipeline | Combines geocoding, OSC, and Commons into one manifest |
| `simple_travel_brochure.py` | All-in-one brochure generator | Handles everything: geocode → fetch → video → plan |

## 🌐 API References

### OpenStreetCam API

- **Base URL**: `https://api.openstreetmap.org/`
- **Documentation**: [API Reference](https://api.openstreetcam.org/api/doc.html)
- **Key Endpoints**:
  - `POST /1.0/list/nearby-photos/` - Get photos near coordinates
  - `POST /nearby-tracks` - Get nearby sequences
- **Authentication**: Not required for read operations

See `references/openstreetcam_api.md` for detailed API documentation.

### Wikimedia Commons API

- **Base URL**: `https://commons.wikimedia.org/w/api.php`
- **Documentation**: [MediaWiki API](https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page)
- **Key Operations**:
  - `action=query&list=search` - Search for images
  - `action=query&prop=imageinfo` - Get image URLs and metadata
- **Authentication**: Not required for read operations

See `references/commons_api.md` for detailed API documentation.

### VLM Run CLI

- **Installation**: `uv pip install "vlmrun[cli]"`
- **Documentation**: See vlmrun-cli-skill for setup and usage
- **Key Command**: `vlmrun chat <prompt> -i <image> ... -o <output>`

## 🐛 Troubleshooting

### Installation Issues

**Python not found:**
- **Windows**: Ensure Python is added to PATH during installation, or use `py` instead of `python`
- **macOS/Linux**: Use `python3` instead of `python`

**uv command not found:**
- Restart your terminal after installation
- **Windows**: Check if uv is in your PATH: `$env:PATH`
- **macOS/Linux**: Ensure `~/.cargo/bin` or `~/.local/bin` is in your PATH

**Virtual environment activation fails:**
- **Windows PowerShell**: If you get an execution policy error, run:
  ```powershell
  Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
  ```
- **Windows CMD**: Use `.venv\Scripts\activate.bat` instead of `.ps1`
- **macOS/Linux**: Ensure you're using `source .venv/bin/activate`

**vlmrun not found:**
- Ensure dependencies are installed: `uv pip install "vlmrun[cli]"`
- Verify installation: `vlmrun --version`

### Runtime Issues

**Geocode fails:**
- Try adding country/region: "Paris, France" instead of just "Paris"
- Use "City, Country" format for better results

**OpenStreetCam returns few/no results:**
- Increase radius parameter (default: 2000m, try 5000m or 10000m)
- Try city center coordinates instead of outskirts
- Some regions have sparse coverage; try nearby major cities

**Commons returns few results:**
- Broaden query (e.g., "City tourism", "City sights", "City landmarks")
- Try different search terms related to the destination

**vlmrun errors:**
- Confirm `VLMRUN_API_KEY` is set correctly:
  ```bash
  # Windows PowerShell
  echo $env:VLMRUN_API_KEY
  
  # macOS/Linux
  echo $VLMRUN_API_KEY
  ```
- Check network connection
- Reduce number of input images if hitting API limits (try 5-10 images instead of 20+)
- Verify API key is valid and has sufficient credits/quota

**Script execution errors:**
- Ensure you're in the correct directory (skill root directory)
- Check that all dependencies are installed: `uv pip list`
- Verify Python version: `python --version` (should be 3.10+)

## 📝 Example Workflows

### Example 1: Quick Brochure Generation

```bash
# Generate a complete travel brochure for Tokyo
uv run scripts/simple_travel_brochure.py --city "Tokyo, Japan" --output ./tokyo_brochure

# Output will be in ./tokyo_brochure/
```

### Example 2: Custom Image Collection

```bash
# Step 1: Collect images with custom parameters
uv run scripts/run_travel_pipeline.py \
  --city "Barcelona, Spain" \
  --output-dir ./barcelona \
  --max-photos 20 \
  --max-commons 15 \
  --radius 5000

# Step 2: Generate video (manually)
vlmrun chat "Create a 30-second travel video of Barcelona" \
  -i ./barcelona/images/img_0000.jpg \
  -i ./barcelona/images/img_0001.jpg \
  -i ./barcelona/images/img_0002.jpg \
  -i ./barcelona/images/img_0003.jpg \
  -i ./barcelona/images/img_0004.jpg \
  -o ./barcelona/video

# Step 3: Generate travel plan (manually)
vlmrun chat "Write a one-day travel plan for Barcelona" \
  -i ./barcelona/images/img_0000.jpg \
  -i ./barcelona/images/img_0001.jpg \
  -i ./barcelona/images/img_0002.jpg \
  -o ./barcelona
```

### Example 3: Step-by-Step Manual Workflow

```bash
# 1. Geocode city
uv run scripts/geocode_city.py "Dubai, UAE"
# Output: {"lat": 25.2048, "lng": 55.2708, "display_name": "Dubai, UAE"}

# 2. Fetch OpenStreetCam photos
uv run scripts/fetch_openstreetcam.py \
  --lat 25.2048 \
  --lng 55.2708 \
  --radius 3000 \
  --output ./dubai/osc \
  --max-photos 10

# 3. Fetch Commons images
uv run scripts/fetch_commons.py \
  --query "Dubai landmarks tourism" \
  --output ./dubai/commons \
  --max-images 8

# 4. Use collected images with vlmrun
vlmrun chat "Create a travel video of Dubai" \
  -i ./dubai/osc/osc_0000.jpg \
  -i ./dubai/commons/... \
  -o ./dubai/video
```

## 📚 Additional Resources

- **OpenStreetCam**: [Website](https://openstreetcam.org/) | [API Docs](https://api.openstreetcam.org/api/doc.html)
- **Wikimedia Commons**: [Website](https://commons.wikimedia.org/) | [API Docs](https://commons.wikimedia.org/w/api.php)
- **Nominatim Geocoding**: [Documentation](https://nominatim.org/release-docs/develop/api/Overview/)
- **VLM Run**: See vlmrun-cli-skill documentation for detailed usage

## ✅ Checklist for Complete Run

- [ ] Python 3.10+ installed and verified
- [ ] Dependencies installed (`vlmrun[cli]`, `requests`)
- [ ] `VLMRUN_API_KEY` set (optional but recommended)
- [ ] User provided destination city (with country if needed)
- [ ] Geocoded city and confirmed lat/lng
- [ ] Fetched OpenStreetCam photos; saved images and manifest
- [ ] Fetched Commons images for the destination; saved images and manifest
- [ ] Built aggregated manifest/images under output directory
- [ ] Ran vlmrun with collected images to generate travel video (if API key set)
- [ ] Ran vlmrun with same images to generate travel plan (if API key set)

## 🤝 Contributing

This is a skill template for AI agents. To extend functionality:

1. Review the existing scripts in `scripts/`
2. Check API references in `references/`
3. Follow the existing code patterns and structure
4. Test with multiple cities and edge cases

## 📄 License

This project uses public APIs and open-source tools. Please respect:
- OpenStreetCam's [Terms of Service](https://openstreetcam.com/terms/)
- Wikimedia Commons' [Licensing](https://commons.wikimedia.org/wiki/Commons:Licensing)
- Nominatim's [Usage Policy](https://operations.osmfoundation.org/policies/nominatim/)

---

**Happy Travel Planning! ✈️🌍**


path: scripts/fetch_commons.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["requests"]
# ///
"""
Search Wikimedia Commons for images and optionally download them.
Usage:
  uv run scripts/fetch_commons.py --query "Paris landmarks" --output ./assets/commons --max-images 15
"""
import argparse
import json
import re
import sys
from pathlib import Path
from urllib.parse import quote

import requests

COMMONS_API = "https://commons.wikimedia.org/w/api.php"
HEADERS = {"User-Agent": "TravelDestinationBrochure/1.0 (skill)"}


def search_commons(query: str, limit: int = 20) -> list[str]:
    """Return list of File: page titles."""
    params = {
        "action": "query",
        "list": "search",
        "srsearch": query,
        "srnamespace": 6,
        "srlimit": limit,
        "format": "json",
    }
    r = requests.get(COMMONS_API, params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    data = r.json()
    items = data.get("query", {}).get("search", [])
    return [x["title"] for x in items]


def get_imageinfo(titles: list[str]) -> dict:
    """Get image URLs and extmetadata for given File: titles."""
    params = {
        "action": "query",
        "titles": "|".join(titles),
        "prop": "imageinfo",
        "iiprop": "url|extmetadata|size|mime",
        "iiurlwidth": 1200,
        "format": "json",
    }
    r = requests.get(COMMONS_API, params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return r.json()


def download_file(url: str, path: Path) -> bool:
    try:
        r = requests.get(url, headers=HEADERS, timeout=30, stream=True)
        r.raise_for_status()
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "wb") as f:
            for chunk in r.iter_content(chunk_size=8192):
                f.write(chunk)
        return True
    except Exception:
        return False


def safe_filename(title: str, index: int) -> str:
    """From File:Example name.jpg -> example_name.jpg."""
    name = title.replace("File:", "").strip()
    name = re.sub(r"[^\w\-_.]", "_", name)[:80]
    if not name:
        name = f"commons_{index}"
    if not name.lower().endswith((".jpg", ".jpeg", ".png", ".webp", ".gif")):
        name += ".jpg"
    return name


def main() -> None:
    ap = argparse.ArgumentParser(description="Fetch Wikimedia Commons images by search query.")
    ap.add_argument("--query", type=str, required=True, help='Search query, e.g. "Paris landmarks"')
    ap.add_argument("--output", type=str, default="./assets/commons", help="Output directory")
    ap.add_argument("--max-images", type=int, default=15, help="Max images to download (default 15)")
    ap.add_argument("--no-download", action="store_true", help="Only build manifest, do not download images")
    args = ap.parse_args()

    out_dir = Path(args.output)
    out_dir.mkdir(parents=True, exist_ok=True)

    try:
        titles = search_commons(args.query, limit=args.max_images)
    except requests.RequestException as e:
        print(f"Commons search failed: {e}", file=sys.stderr)
        sys.exit(1)

    if not titles:
        print(f"No images found for: {args.query}", file=sys.stderr)
        sys.exit(1)

    try:
        info_resp = get_imageinfo(titles)
    except requests.RequestException as e:
        print(f"Commons imageinfo failed: {e}", file=sys.stderr)
        sys.exit(1)

    pages = info_resp.get("query", {}).get("pages", {})
    manifest = []
    for i, (pid, p) in enumerate(pages.items()):
        if i >= args.max_images:
            break
        if pid == "-1" or "missing" in p:
            continue
        ii = (p.get("imageinfo") or [None])[0]
        if not ii:
            continue
        url = ii.get("url") or ii.get("thumburl") or ""
        if not url:
            continue
        extmeta = ii.get("extmetadata") or {}
        desc = (extmeta.get("ImageDescription") or {}).get("value") or ""
        obj = (extmeta.get("ObjectName") or {}).get("value") or ""
        caption = (obj or desc or p.get("title", "")).replace("File:", "").strip()
        if len(caption) > 500:
            caption = caption[:497] + "..."
        title = p.get("title", "File:unknown.jpg")
        entry = {"index": i, "title": title, "url": url, "caption": caption or title}
        if not args.no_download:
            fname = out_dir / safe_filename(title, i)
            if download_file(url, fname):
                entry["path"] = str(fname.resolve())
            else:
                entry["path"] = None
        manifest.append(entry)

    manifest_path = out_dir / "commons_manifest.json"
    with open(manifest_path, "w", encoding="utf-8") as f:
        json.dump({"query": args.query, "images": manifest}, f, indent=2)

    print(json.dumps({"count": len(manifest), "manifest": str(manifest_path), "output_dir": str(out_dir)}, indent=2))


if __name__ == "__main__":
    main()


path: scripts/fetch_openstreetcam.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["requests"]
# ///
"""
Fetch nearby photos from OpenStreetCam API and optionally download them.
Usage:
  uv run scripts/fetch_openstreetcam.py --lat 48.8566 --lng 2.3522 --radius 2000 --output ./assets/osc --max-photos 20
"""
import argparse
import json
import sys
from pathlib import Path

import requests

OSC_BASE = "https://api.openstreetcam.org"
OSC_PHOTO_BASE = "https://api.openstreetcam.org/"
NEARBY_PHOTOS = f"{OSC_BASE}/1.0/list/nearby-photos/"
HEADERS = {"User-Agent": "TravelDestinationBrochure/1.0 (skill)"}


def fetch_nearby_photos(lat: float, lng: float, radius: float, page: int = 1, ipp: int = 50) -> dict:
    data = {"lat": lat, "lng": lng, "radius": radius, "page": page, "ipp": ipp}
    r = requests.post(NEARBY_PHOTOS, data=data, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return r.json()


def download_file(url: str, path: Path) -> bool:
    try:
        r = requests.get(url, headers=HEADERS, timeout=30, stream=True)
        r.raise_for_status()
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "wb") as f:
            for chunk in r.iter_content(chunk_size=8192):
                f.write(chunk)
        return True
    except Exception:
        return False


def main() -> None:
    ap = argparse.ArgumentParser(description="Fetch OpenStreetCam photos near a point.")
    ap.add_argument("--lat", type=float, required=True, help="Latitude")
    ap.add_argument("--lng", type=float, required=True, help="Longitude")
    ap.add_argument("--radius", type=float, default=2000, help="Radius in meters (default 2000)")
    ap.add_argument("--output", type=str, default="./assets/osc", help="Output directory")
    ap.add_argument("--max-photos", type=int, default=20, help="Max photos to download (default 20)")
    ap.add_argument("--no-download", action="store_true", help="Only build manifest, do not download images")
    args = ap.parse_args()

    out_dir = Path(args.output)
    out_dir.mkdir(parents=True, exist_ok=True)

    try:
        resp = fetch_nearby_photos(args.lat, args.lng, args.radius, page=1, ipp=min(100, args.max_photos + 10))
    except requests.RequestException as e:
        print(f"OpenStreetCam request failed: {e}", file=sys.stderr)
        sys.exit(1)

    status = resp.get("status", {})
    if status.get("httpCode") != 200:
        print(f"API error: {status.get('apiMessage', resp)}", file=sys.stderr)
        sys.exit(1)

    # Paginated structure: currentPageItems can be list of photos or nested
    raw_items = resp.get("currentPageItems") or resp.get("result") or []
    if isinstance(raw_items, dict):
        items = raw_items.get("data", []) if isinstance(raw_items.get("data"), list) else []
    else:
        items = raw_items if isinstance(raw_items, list) else []

    # Normalize: OSC may return items with name/lth_name/th_name, or nested osv.photos
    photos = []
    for it in items:
        if isinstance(it, dict):
            if "lth_name" in it or "name" in it or "th_name" in it:
                photos.append(it)
            elif "photos" in it:
                photos.extend(it["photos"] if isinstance(it["photos"], list) else [])
        if len(photos) >= args.max_photos:
            break
    photos = photos[: args.max_photos]

    manifest = []
    for i, ph in enumerate(photos):
        # Prefer large thumbnail or full image URL
        url = ph.get("lth_name") or ph.get("name") or ph.get("th_name") or ""
        if not url:
            continue
        # Construct full URL if relative
        if not url.startswith("http"):
            url = OSC_PHOTO_BASE + url
        caption = f"OpenStreetCam photo at lat {ph.get('lat')}, lng {ph.get('lng')}"
        if ph.get("date_added"):
            caption += f" ({ph.get('date_added')})"
        entry = {"index": i, "url": url, "lat": ph.get("lat"), "lng": ph.get("lng"), "caption": caption}
        if not args.no_download:
            ext = "jpg" if "jpg" in url.lower() or "jpeg" in url.lower() else "jpg"
            fname = out_dir / f"osc_{i:04d}.{ext}"
            if download_file(url, fname):
                entry["path"] = str(fname.resolve())
            else:
                entry["path"] = None
        manifest.append(entry)

    manifest_path = out_dir / "osc_manifest.json"
    with open(manifest_path, "w", encoding="utf-8") as f:
        json.dump({"lat": args.lat, "lng": args.lng, "radius": args.radius, "photos": manifest}, f, indent=2)

    print(json.dumps({"count": len(manifest), "manifest": str(manifest_path), "output_dir": str(out_dir)}, indent=2))


if __name__ == "__main__":
    main()


path: scripts/geocode_city.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["requests"]
# ///
"""
Geocode a city name to lat/lng using Nominatim (OpenStreetMap).
Usage: uv run scripts/geocode_city.py "Paris, France"
       uv run scripts/geocode_city.py "Tokyo" --json
"""
import argparse
import json
import sys
from urllib.parse import quote

import requests

NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"
HEADERS = {"User-Agent": "TravelDestinationBrochure/1.0 (skill)"}


def geocode(query: str, limit: int = 1) -> list[dict]:
    params = {"q": query, "format": "json", "limit": limit}
    r = requests.get(NOMINATIM_URL, params=params, headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()


def main() -> None:
    ap = argparse.ArgumentParser(description="Geocode a city name to latitude/longitude.")
    ap.add_argument("city", help='City name, e.g. "Paris, France" or "Tokyo"')
    ap.add_argument("--limit", type=int, default=1, help="Max results (default 1)")
    ap.add_argument("--json", action="store_true", help="Output raw JSON only")
    args = ap.parse_args()

    try:
        results = geocode(args.city, limit=args.limit)
    except requests.RequestException as e:
        print(f"Geocoding failed: {e}", file=sys.stderr)
        sys.exit(1)

    if not results:
        print(f"No results for: {args.city}", file=sys.stderr)
        sys.exit(1)

    if args.json:
        print(json.dumps(results, indent=2))
        return

    first = results[0]
    lat = float(first["lat"])
    lon = float(first["lon"])
    display = first.get("display_name", "")
    out = {"lat": lat, "lng": lon, "display_name": display}
    print(json.dumps(out, indent=2))


if __name__ == "__main__":
    main()


path: scripts/run_travel_pipeline.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["requests"]
# ///
"""
Full pipeline: geocode city -> fetch OpenStreetCam photos -> fetch Commons images -> write combined manifest and images.
Usage:
  uv run scripts/run_travel_pipeline.py --city "Paris, France" --output-dir ./travel_output
  uv run scripts/run_travel_pipeline.py --city "Tokyo" --output-dir ./travel_output --max-photos 10 --max-commons 8
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path

# Assumes scripts are next to this file
SCRIPT_DIR = Path(__file__).resolve().parent


def run_cmd(cmd: list, cwd: Path | None = None) -> tuple[bool, str]:
    try:
        r = subprocess.run(
            cmd,
            cwd=cwd or SCRIPT_DIR.parent,
            capture_output=True,
            text=True,
            timeout=120,
        )
        return r.returncode == 0, (r.stdout or "") + (r.stderr or "")
    except Exception as e:
        return False, str(e)


def main() -> None:
    ap = argparse.ArgumentParser(description="Run full travel brochure pipeline: geocode -> OSC -> Commons -> manifest.")
    ap.add_argument("--city", type=str, required=True, help='Destination city, e.g. "Paris, France"')
    ap.add_argument("--output-dir", type=str, default="./travel_output", help="Output directory for images and manifest")
    ap.add_argument("--radius", type=float, default=2000, help="OpenStreetCam radius in meters (default 2000)")
    ap.add_argument("--max-photos", type=int, default=15, help="Max OpenStreetCam photos (default 15)")
    ap.add_argument("--max-commons", type=int, default=10, help="Max Commons images (default 10)")
    ap.add_argument("--skip-osc", action="store_true", help="Skip OpenStreetCam fetch")
    ap.add_argument("--skip-commons", action="store_true", help="Skip Wikimedia Commons fetch")
    args = ap.parse_args()

    out_dir = Path(args.output_dir)
    images_dir = out_dir / "images"
    images_dir.mkdir(parents=True, exist_ok=True)

    # 1) Geocode
    ok, out = run_cmd([sys.executable, str(SCRIPT_DIR / "geocode_city.py"), args.city])
    if not ok:
        print(f"Geocode failed: {out}", file=sys.stderr)
        sys.exit(1)
    try:
        geocode_line = [l for l in out.strip().splitlines() if l.strip().startswith("{")][-1]
        geo = json.loads(geocode_line)
        lat = geo["lat"]
        lng = geo["lng"]
        display = geo.get("display_name", args.city)
    except (IndexError, KeyError, json.JSONDecodeError):
        print("Could not parse geocode output:", out, file=sys.stderr)
        sys.exit(1)

    all_entries = []
    prefix = 0

    # 2) OpenStreetCam
    if not args.skip_osc:
        osc_dir = out_dir / "osc"
        osc_dir.mkdir(parents=True, exist_ok=True)
        ok, out = run_cmd([
            sys.executable, str(SCRIPT_DIR / "fetch_openstreetcam.py"),
            "--lat", str(lat), "--lng", str(lng),
            "--radius", str(args.radius), "--output", str(osc_dir),
            "--max-photos", str(args.max_photos),
        ])
        if ok:
            manifest_path = osc_dir / "osc_manifest.json"
            if manifest_path.exists():
                with open(manifest_path, encoding="utf-8") as f:
                    osc_data = json.load(f)
                for p in osc_data.get("photos", []):
                    path = p.get("path")
                    if path:
                        dest = images_dir / f"img_{prefix:04d}.jpg"
                        try:
                            Path(path).replace(dest)
                        except OSError:
                            import shutil
                            shutil.copy2(path, dest)
                        all_entries.append({"path": str(dest), "source": "openstreetcam", "caption": p.get("caption", "")})
                        prefix += 1
        else:
            print(f"OpenStreetCam fetch warning: {out[:200]}", file=sys.stderr)

    # 3) Commons
    if not args.skip_commons:
        commons_dir = out_dir / "commons"
        commons_dir.mkdir(parents=True, exist_ok=True)
        query = f"{args.city} landmarks tourism"
        ok, out = run_cmd([
            sys.executable, str(SCRIPT_DIR / "fetch_commons.py"),
            "--query", query, "--output", str(commons_dir),
            "--max-images", str(args.max_commons),
        ])
        if ok:
            manifest_path = commons_dir / "commons_manifest.json"
            if manifest_path.exists():
                with open(manifest_path, encoding="utf-8") as f:
                    comm_data = json.load(f)
                for p in comm_data.get("images", []):
                    path = p.get("path")
                    if path:
                        dest = images_dir / f"img_{prefix:04d}.jpg"
                        try:
                            Path(path).replace(dest)
                        except OSError:
                            import shutil
                            shutil.copy2(path, dest)
                        all_entries.append({"path": str(dest), "source": "commons", "caption": p.get("caption", "")})
                        prefix += 1
        else:
            print(f"Commons fetch warning: {out[:200]}", file=sys.stderr)

    if not all_entries:
        print("No images collected. Check OSC/Commons outputs.", file=sys.stderr)
        sys.exit(1)

    # 4) Combined manifest
    manifest = {
        "city": args.city,
        "display_name": display,
        "lat": lat,
        "lng": lng,
        "images": all_entries,
    }
    manifest_path = out_dir / "manifest.json"
    with open(manifest_path, "w", encoding="utf-8") as f:
        json.dump(manifest, f, indent=2)

    # 5) vlmrun hint file (list of paths for easy paste into CLI)
    paths_file = out_dir / "image_paths.txt"
    with open(paths_file, "w", encoding="utf-8") as f:
        for e in all_entries:
            f.write(e["path"] + "\n")

    print(json.dumps({
        "city": args.city,
        "lat": lat,
        "lng": lng,
        "image_count": len(all_entries),
        "manifest": str(manifest_path),
        "images_dir": str(images_dir),
        "image_paths_file": str(paths_file),
        "next_step": "Run vlmrun chat with -i for each image in images_dir to generate video and travel plan.",
    }, indent=2))


if __name__ == "__main__":
    main()


path: scripts/simple_travel_brochure.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["requests"]
# ///
"""
Simplified travel brochure generator: gets 5 images and generates video with vlmrun.
Usage:
  uv run scripts/simple_travel_brochure.py --city "Doha, Qatar"
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
import requests

SCRIPT_DIR = Path(__file__).resolve().parent
OSC_BASE = "https://api.openstreetcam.org"
OSC_PHOTO_BASE = "https://api.openstreetcam.org/"
NEARBY_PHOTOS = f"{OSC_BASE}/1.0/list/nearby-photos/"
COMMONS_API = "https://commons.wikimedia.org/w/api.php"
NOMINATIM = "https://nominatim.openstreetmap.org/search"
HEADERS = {"User-Agent": "TravelBrochureSkill/1.0"}


def geocode_city(city: str) -> dict:
    """Geocode city name to lat/lng."""
    params = {"q": city, "format": "json", "limit": 1}
    r = requests.get(NOMINATIM, params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    results = r.json()
    if not results:
        raise ValueError(f"City not found: {city}")
    return {
        "lat": float(results[0]["lat"]),
        "lng": float(results[0]["lon"]),
        "display_name": results[0].get("display_name", city)
    }


def fetch_osc_photos(lat: float, lng: float, count: int = 3) -> list:
    """Fetch OpenStreetCam photos."""
    data = {"lat": lat, "lng": lng, "radius": 3000, "page": 1, "ipp": count + 5}
    r = requests.post(NEARBY_PHOTOS, data=data, headers=HEADERS, timeout=30)
    r.raise_for_status()
    resp = r.json()

    raw_items = resp.get("currentPageItems") or resp.get("result") or []
    if isinstance(raw_items, dict):
        items = raw_items.get("data", [])
    else:
        items = raw_items

    photos = []
    for item in items:
        if isinstance(item, dict):
            url = item.get("lth_name") or item.get("name") or item.get("th_name")
            if url:
                if not url.startswith("http"):
                    url = OSC_PHOTO_BASE + url
                photos.append({
                    "url": url,
                    "caption": f"Street view at {item.get('lat')}, {item.get('lng')}"
                })
        if len(photos) >= count:
            break

    return photos[:count]


def fetch_commons_images(query: str, count: int = 2) -> list:
    """Fetch Wikimedia Commons images."""
    # Search for files
    search_params = {
        "action": "query",
        "list": "search",
        "srsearch": query,
        "srnamespace": 6,
        "srlimit": count + 10,
        "format": "json"
    }
    r = requests.get(COMMONS_API, params=search_params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    search_results = r.json()

    titles = [item["title"] for item in search_results.get("query", {}).get("search", [])][:count + 5]
    if not titles:
        return []

    # Get image info
    info_params = {
        "action": "query",
        "titles": "|".join(titles),
        "prop": "imageinfo",
        "iiprop": "url|extmetadata",
        "format": "json"
    }
    r = requests.get(COMMONS_API, params=info_params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    info_resp = r.json()

    images = []
    pages = info_resp.get("query", {}).get("pages", {})
    for pid, page in pages.items():
        if pid == "-1" or "missing" in page:
            continue
        ii = (page.get("imageinfo") or [None])[0]
        if not ii:
            continue
        url = ii.get("url")
        if not url or url.lower().endswith(".pdf"):
            continue

        extmeta = ii.get("extmetadata") or {}
        desc = (extmeta.get("ImageDescription") or {}).get("value") or ""
        obj = (extmeta.get("ObjectName") or {}).get("value") or ""
        caption = (obj or desc or page.get("title", "")).replace("File:", "").strip()
        if len(caption) > 200:
            caption = caption[:197] + "..."

        images.append({"url": url, "caption": caption})
        if len(images) >= count:
            break

    return images[:count]


def download_image(url: str, path: Path) -> bool:
    """Download image to path."""
    try:
        r = requests.get(url, headers=HEADERS, timeout=30, stream=True)
        r.raise_for_status()
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "wb") as f:
            for chunk in r.iter_content(chunk_size=8192):
                f.write(chunk)
        return True
    except Exception as e:
        print(f"Download failed for {url}: {e}", file=sys.stderr)
        return False


def generate_video(images_dir: Path, city: str, output_dir: Path) -> bool:
    """Generate travel video using vlmrun."""
    image_files = sorted(images_dir.glob("*.jpg"))[:5]
    if not image_files:
        print("No images found to generate video", file=sys.stderr)
        return False

    # Build vlmrun command
    prompt = f"Create a 30-second travel video showcasing {city}. Use these images to highlight the city's beauty and culture. Add smooth transitions and subtle captions. Use an inspiring, calm travel documentary style."

    cmd = ["vlmrun", "chat", prompt]
    for img in image_files:
        cmd.extend(["-i", str(img)])
    cmd.extend(["-o", str(output_dir / "video")])

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
        if result.returncode == 0:
            print("   Video generated successfully!")
            return True
        else:
            # Check if it's an API key error
            if "API key not found" in result.stderr:
                print("   Skipped: VLMRUN_API_KEY not set", file=sys.stderr)
            else:
                print(f"   Error: vlmrun failed (code {result.returncode})", file=sys.stderr)
            return False
    except FileNotFoundError:
        print("   Skipped: vlmrun not installed", file=sys.stderr)
        return False
    except Exception as e:
        print(f"   Error: {e}", file=sys.stderr)
        return False


def generate_travel_plan(images_dir: Path, city: str, output_dir: Path) -> bool:
    """Generate travel plan using vlmrun."""
    image_files = sorted(images_dir.glob("*.jpg"))[:5]
    if not image_files:
        return False

    prompt = f"Based on these images of {city}, create a detailed one-day travel plan with morning, midday, and evening activities. Include specific places to visit, local tips, and practical recommendations. Format as structured markdown."

    cmd = ["vlmrun", "chat", prompt]
    for img in image_files:
        cmd.extend(["-i", str(img)])
    cmd.extend(["-o", str(output_dir)])

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
        if result.returncode == 0:
            # Save the plan
            plan_file = output_dir / "travel_plan.md"
            with open(plan_file, "w", encoding="utf-8") as f:
                f.write(result.stdout)
            print(f"   Travel plan saved to {plan_file}")
            return True
        else:
            # Check if it's an API key error
            if "API key not found" in result.stderr:
                print("   Skipped: VLMRUN_API_KEY not set", file=sys.stderr)
            else:
                print(f"   Error: vlmrun failed (code {result.returncode})", file=sys.stderr)
            return False
    except FileNotFoundError:
        print("   Skipped: vlmrun not installed", file=sys.stderr)
        return False
    except Exception as e:
        print(f"   Error: {e}", file=sys.stderr)
        return False


def cleanup_temp_files(temp_dir: Path):
    """Remove temporary files."""
    if temp_dir.exists():
        shutil.rmtree(temp_dir)
        print(f"Cleaned up temporary files in {temp_dir}")


def main():
    ap = argparse.ArgumentParser(description="Generate travel brochure with 5 images and video")
    ap.add_argument("--city", type=str, required=True, help='City name, e.g. "Doha, Qatar"')
    ap.add_argument("--output", type=str, default="./travel_brochure", help="Output directory")
    ap.add_argument("--osc-count", type=int, default=3, help="Number of OpenStreetCam photos (default 3)")
    ap.add_argument("--commons-count", type=int, default=2, help="Number of Commons images (default 2)")
    args = ap.parse_args()

    output_dir = Path(args.output)
    images_dir = output_dir / "images"
    images_dir.mkdir(parents=True, exist_ok=True)

    print(f"\n[*] Generating travel brochure for: {args.city}")

    # Step 1: Geocode
    print("[1/5] Geocoding city...")
    try:
        geo = geocode_city(args.city)
        # Try to print display name, but skip if encoding issues
        try:
            print(f"   Found: {geo['display_name']}")
        except UnicodeEncodeError:
            print(f"   Found: {args.city} (lat: {geo['lat']:.4f}, lng: {geo['lng']:.4f})")
    except Exception as e:
        print(f"ERROR: Geocoding failed: {e}", file=sys.stderr)
        sys.exit(1)

    # Step 2: Fetch images
    print(f"[2/5] Fetching {args.osc_count} street photos from OpenStreetCam...")
    try:
        osc_photos = fetch_osc_photos(geo["lat"], geo["lng"], args.osc_count)
        print(f"   Got {len(osc_photos)} photos")
    except Exception as e:
        print(f"WARNING: OpenStreetCam failed: {e}", file=sys.stderr)
        osc_photos = []

    print(f"[2/5] Fetching {args.commons_count} landmark images from Wikimedia Commons...")
    try:
        commons_images = fetch_commons_images(f"{args.city} landmarks", args.commons_count)
        print(f"   Got {len(commons_images)} images")
    except Exception as e:
        print(f"WARNING: Commons failed: {e}", file=sys.stderr)
        commons_images = []

    # Step 3: Download images
    all_images = osc_photos + commons_images
    if len(all_images) < 3:
        print(f"ERROR: Not enough images collected ({len(all_images)}), need at least 3", file=sys.stderr)
        sys.exit(1)

    print(f"[3/5] Downloading {len(all_images)} images...")
    downloaded = 0
    for i, img in enumerate(all_images[:5]):
        dest = images_dir / f"image_{i:02d}.jpg"
        if download_image(img["url"], dest):
            downloaded += 1
            print(f"   [OK] Image {i+1}/5")

    if downloaded < 3:
        print(f"ERROR: Only downloaded {downloaded} images, need at least 3", file=sys.stderr)
        sys.exit(1)

    print(f"\nSuccessfully downloaded {downloaded} images")

    # Step 4: Generate video
    print("[4/5] Generating travel video with vlmrun...")
    video_success = generate_video(images_dir, args.city, output_dir)

    # Step 5: Generate travel plan
    print("[5/5] Generating travel plan with vlmrun...")
    plan_success = generate_travel_plan(images_dir, args.city, output_dir)

    # Step 6: Save manifest
    manifest = {
        "city": args.city,
        "display_name": geo["display_name"],
        "lat": geo["lat"],
        "lng": geo["lng"],
        "images": [{"path": str(p), "caption": all_images[i].get("caption", "")}
                   for i, p in enumerate(sorted(images_dir.glob("*.jpg"))[:5])],
        "video_generated": video_success,
        "plan_generated": plan_success
    }

    manifest_file = output_dir / "manifest.json"
    with open(manifest_file, "w", encoding="utf-8") as f:
        json.dump(manifest, f, indent=2)

    # Step 7: Cleanup (optional - remove intermediate API response files if any)
    # Images are kept in output_dir/images/

    print(f"\nDone! Results saved to: {output_dir.resolve()}")
    print(f"   Manifest: {manifest_file}")
    if video_success:
        print(f"   Video: {output_dir / 'video'}")
    if plan_success:
        print(f"   Travel plan: {output_dir / 'travel_plan.md'}")

    return 0


if __name__ == "__main__":
    sys.exit(main())


path: references/commons_api.md
content: # Wikimedia Commons API Reference

Source: [Commons API](https://commons.wikimedia.org/w/api.php), [MediaWiki API](https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page)

## Base URL

- **Commons API:** `https://commons.wikimedia.org/w/api.php`

## Common parameters

- `action=query` – Fetch data.
- `format=json` – Prefer JSON.
- `origin=*` – For CORS from browsers if needed.

## Search for images (files)

- **action=query**  
  **list=search**  
  **srsearch=**&lt;query&gt; e.g. "Paris Eiffel Tower"  
  **srnamespace=6** – File namespace (images on Commons).  
  **srlimit=**&lt;number&gt;  
  **srwhat=** (e.g. text, title, nearmatch)

Example:

```
GET https://commons.wikimedia.org/w/api.php?action=query&list=search&srsearch=Paris+landmarks&srnamespace=6&srlimit=20&format=json
```

Response: `query.search[]` with `title` (e.g. "File:Example.jpg"), `pageid`, etc.

## Get image URLs and metadata

- **action=query**  
  **titles=**File:Example.jpg (pipe-separated for multiple)  
  **prop=imageinfo**  
  **iiprop=url|extmetadata|size|mime**  
  **iiurlwidth=** (optional, for thumbnail width)

Example:

```
GET https://commons.wikimedia.org/w/api.php?action=query&titles=File:Example.jpg&prop=imageinfo&iiprop=url|extmetadata&format=json
```

Response: `query.pages[pageid].imageinfo[0].url` (direct image URL), `extmetadata` (ImageDescription, Artist, LicenseShortName, etc.).

## Continuation (many results)

- Use **list=search** with **sroffset** for pagination.
- Use **query-continue** in response (or **continue** in API) when returned to get next page.

## Usage in this skill

1. **list=search** with `srsearch=<city name> landmarks` (or "tourist", "sights") and `srnamespace=6` to get file titles.
2. **prop=imageinfo** with **iiprop=url|extmetadata** for those titles to get download URL and captions/descriptions.
3. Download images from `imageinfo[].url` and store captions from `extmetadata.ImageDescription.value` or `extmetadata.ObjectName.value` for the travel brochure/video.


path: references/openstreetcam_api.md
content: # OpenStreetCam API Reference

Source: [OpenStreetCam API Doc](https://api.openstreetcam.org/api/doc.html)

## Base URL

- **API:** `https://api.openstreetcam.org/`
- **Terms:** https://openstreetcam.com/terms/

## Authentication

Required only for **upload** and **my-list** operations. For **nearby-tracks** and **nearby-photos** no token is needed.

- **POST /auth/openstreetmap/client_auth**  
  Body (formData): `request_token`, `secret_token` (from OSM OAuth).  
  Returns: `access_token` for use as `access_token` in other requests.

## Map / Display (no auth)

### Nearby sequences

- **POST /nearby-tracks**
- Body (formData): `lat` (number), `lng` (number), `distance` (number, radius in km).
- Response: `osv` (Nearby)`, `status`.  
  `Nearby` has `lat`, `lng`, `from`, `to`, `way_id`, `sequences[]`.

### Nearby photos

- **POST /1.0/list/nearby-photos/**
- Body (formData): `lat`, `lng`, `radius` (meters). Optional: `heading`, `wayId`, `page`, `ipp`, `externalUserId`, `date`.
- Response: paginated; includes `currentPageItems`, `totalFilteredItems`, `tracksStatus`. Each photo item has: `id`, `lat`, `lng`, `name` (full image URL), `lth_name` (large thumbnail), `th_name` (small thumbnail), `sequence_id`, `sequence_index`, `date_added`, `heading`, etc.

### Matched tracks (bounding box)

- **POST /1.0/tracks/**
- Body: `bbTopLeft` (`'lat,lng'`), `bbBottomRight` (`'lat,lng'`). Optional: `obdInfo`, `platform`, `page`, `ipp`, `zoom`, `requestedParams`.
- Use to get segments (polylines) with coverage in a bbox.

## Sequence (auth required for create/delete)

- **POST /1.0/sequence/** – Create (metaData file, uploadSource, etc.).
- **POST /details** – Get sequence details (formData: `id`).
- **POST /1.0/sequence/photo-list/** – Photo list for sequence (`sequenceId`, `access_token`).
- **POST /1.0/sequence/remove/** – Remove sequence.
- **POST /1.0/sequence/finished-uploading/** – Mark upload finished.
- **POST /1.0/list/my-list** – List of user's sequences (bbox, filters, `access_token`).

## Photo (auth for upload/remove)

- **POST /1.0/photo/** – Upload photo (sequenceId, sequenceIndex, coordinate, gpsAccuracy, photo file, `access_token`).
- **POST /1.0/photo/remove/** – Remove photo (`photoId`, `access_token`).

## Video (auth)

- **POST /1.0/video/** – Upload video to sequence (`sequenceId`, `sequenceIndex`, video file, `access_token`).

## Usage in this skill

Use only read endpoints:

1. **Geocode** city → (lat, lng).
2. **POST /nearby-tracks** with (lat, lng, distance) to discover sequences.
3. **POST /1.0/list/nearby-photos/** with (lat, lng, radius, page, ipp) to get photo list; then download from `name` or `th_name`/`lth_name` URLs.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)