A self-hosted filming-location workspace for filmmakers and small production teams.
FastAPI
· PostgreSQL
· SQLAlchemy 2
· Leaflet
· OpenAI
· Raspberry Pi
FrameScout turns scattered map bookmarks, phone photos and production notes into one focused workspace. A filmmaker can save a location, document access and sound considerations, attach reference images, inspect wind and natural light, and ask an AI assistant to compare saved places.
This is a production-minded portfolio project rather than a mockup. It covers the full lifecycle of a web application: authenticated REST APIs, relational data modelling, external integrations, privacy controls, safe image handling, responsive frontend work and a self-hosted Raspberry Pi deployment.
Docker Compose starts the FastAPI application and PostgreSQL. The initial database schema is applied automatically when the PostgreSQL volume is created for the first time.
Copy-Item .env.example .env
# Edit .env and replace DB_PASSWORD and AUTH_SECRET_KEY with real values.
docker compose up --buildOpen http://localhost:8010. Stop the stack with Ctrl+C. The application
stores uploaded photos in the local uploads/ directory and PostgreSQL data
in a named Docker volume.
For a clean disposable database during local development:
docker compose down -vThat command removes the Docker database volume and is not intended for a production deployment.
- Leaflet map with deliberate add-location mode
- Create, list, edit and delete saved filming locations
- Production notes, coordinates and drone airspace reminders
- Google Street View links
- Responsive desktop and mobile layouts
- Current temperature and wind from OpenWeather
- Sunrise, sunset, golden hour and blue hour from Sunrise-Sunset API
- PostgreSQL-backed weather cache with stale-data fallback
- Same-day sunlight cache
- Manual weather refresh with frontend and backend cooldown protection
- Visible timestamps showing whether weather came from cache or is stale
- JPEG, PNG, WebP, HEIC and MPO upload support
- 10 MB file-size and 50-million-pixel limits
- EXIF removal and server-side image normalization
- In-page gallery and lightbox viewer
- Per-user ownership checks for locations, photos and conditions
- Account export as a ZIP file
- Account and associated data deletion
- Privacy notice and terms of use pages
The FrameScout AI assistant is grounded in the signed-in user's saved locations. It can compare places, interpret current conditions and answer follow-up questions in the selected UI language.
The assistant includes several practical safeguards:
- only the current user's locations are placed in the context;
- at most 50 locations are considered and live conditions are fetched for at most 10 of them;
- weather and sunlight data use the PostgreSQL cache when possible;
- obvious unrelated requests are rejected before an OpenAI request;
- per-user, global daily and concurrent-request limits protect cost and abuse;
- conversation history is bounded and treated as untrusted reference data;
- saved locations are returned as safe clickable UI references rather than raw HTML or Markdown.
The API key stays on the server in .env; it is never sent to the browser.
flowchart LR
Browser["Responsive browser UI<br/>HTML, CSS, JavaScript, Leaflet"]
Funnel["Tailscale Funnel<br/>public HTTPS"]
API["FastAPI application<br/>REST, auth, validation"]
DB[("PostgreSQL")]
Cache["Conditions cache<br/>AI request logs"]
Files[("Private photo storage")]
Google["Google OpenID Connect"]
Weather["OpenWeather API"]
Sun["Sunrise-Sunset API"]
OpenAI["OpenAI Responses API"]
Browser --> Funnel --> API
API --> DB
DB --> Cache
API --> Files
API <--> Google
API --> Weather
API --> Sun
API --> OpenAI
The browser never talks directly to PostgreSQL or external AI/weather providers. FastAPI is the boundary for authentication, ownership checks, validation, rate limits and integration secrets.
For the engineering decisions behind the system, see the technical overview.
app/
├── main.py # FastAPI routes and application orchestration
├── database.py # PostgreSQL engine and request-scoped sessions
├── models.py # SQLAlchemy models
├── schemas.py # Pydantic request and response validation
├── security.py # Argon2 hashing and JWT cookies
├── oauth.py # Google OpenID Connect configuration
├── services/
│ ├── ai.py # OpenAI Responses API integration
│ ├── ai_context.py # Scope checks and bounded location context
│ ├── ai_conditions.py # Concurrent AI condition enrichment
│ ├── ai_limits.py # AI usage limits and request logs
│ ├── conditions_cache.py # PostgreSQL weather and sun cache
│ ├── weather.py # OpenWeather integration
│ └── sun.py # Sunrise-Sunset integration
└── templates/ # Privacy and terms pages
database/
├── schema.sql # Complete initial PostgreSQL schema
└── migrations/ # Incremental SQL migrations
frontend/
├── index.html
├── styles.css
├── app.js
└── assets/
- Python 3.13 or newer
- PostgreSQL 18 or a compatible PostgreSQL server
- Node.js for JavaScript syntax checks
- Git
git clone https://github.com/Lanque/FrameScout.git
cd FrameScout
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txtOn Linux or macOS:
source .venv/bin/activateCreate a PostgreSQL login named drone_app and a database named
drone_locations, owned by that login. Apply
database/schema.sql:
createuser --pwprompt drone_app
createdb --owner=drone_app drone_locations
psql --dbname=drone_locations --file=database/schema.sqlIf you are upgrading an existing installation, run the relevant migration as the PostgreSQL administrator. For the current AI and conditions changes:
sudo -u postgres psql \
-v ON_ERROR_STOP=1 \
-d drone_locations \
< database/migrations/003_conditions_cache_and_ai_limits.sqlCopy-Item .env.example .envAt minimum, set the database values and a long random AUTH_SECRET_KEY.
OpenWeather, Google login and OpenAI are optional integrations.
Generate a suitable secret with:
python -c "import secrets; print(secrets.token_urlsafe(48))"Never commit .env. The OpenAI key belongs only in the server-side environment.
uvicorn app.main:app --reload --host 127.0.0.1 --port 8010Open http://127.0.0.1:8010 or the interactive API documentation at http://127.0.0.1:8010/docs.
| Variable | Required | Purpose |
|---|---|---|
DB_HOST, DB_PORT, DB_NAME |
Yes | PostgreSQL connection target |
DB_USER, DB_PASSWORD |
Yes | Restricted application database login |
AUTH_SECRET_KEY |
Yes | Signs authentication tokens |
OPENWEATHER_API_KEY |
No | Enables current wind and temperature |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
No | Enables Google login |
OAUTH_SESSION_SECRET |
No | Separate OAuth session secret |
COOKIE_SECURE |
Production | Enables secure cookies behind HTTPS |
OPENAI_API_KEY |
No | Enables the AI assistant |
OPENAI_MODEL |
No | OpenAI model, default gpt-5.4-mini |
AI_USER_LIMIT_15_MINUTES |
No | Per-user rolling-window AI limit |
AI_USER_DAILY_LIMIT |
No | Per-user daily AI limit |
AI_GLOBAL_DAILY_LIMIT |
No | Global daily AI limit |
AI_MAX_CONCURRENT_REQUESTS |
No | Concurrent AI request ceiling |
AI_TEST_ENABLED |
No | Enables the disabled-by-default /ai/test endpoint |
AI_SAFETY_SALT |
No | Stable privacy-preserving provider identifier salt |
WEATHER_CACHE_SECONDS |
No | Fresh-weather cache lifetime |
WEATHER_STALE_SECONDS |
No | Maximum stale-weather fallback age |
WEATHER_REFRESH_COOLDOWN_SECONDS |
No | Manual refresh cooldown |
LEGAL_CONTROLLER_NAME, LEGAL_CONTACT_EMAIL |
Public deployment | Legal page contact details |
The same checks run locally and in GitHub Actions:
python -m compileall app
node --check frontend/app.js
python -m pip check
git diff --checkHealth endpoints:
GET /health
GET /health/database
The current production-style deployment runs on a Raspberry Pi 5:
- Debian 13
- PostgreSQL 18
- Uvicorn managed by systemd
- application bound to
127.0.0.1:8010 - public HTTPS provided by Tailscale Funnel
- secrets stored in a server-side
.env - uploaded photos stored outside Git under
uploads/
Typical update flow:
cd /home/lanque/framescout
git pull --ff-only
sudo systemctl restart framescout
sudo systemctl status framescout --no-pager
curl http://127.0.0.1:8010/health- Designing a relational model and authenticated CRUD API
- Enforcing ownership server-side instead of trusting frontend state
- Separating ORM models, validation schemas and service integrations
- Coordinating asynchronous external requests with bounded concurrency
- Designing a cache with stale-data fallback and explicit refresh protection
- Handling untrusted photo uploads, EXIF privacy and filesystem cleanup
- Building a map-based responsive interface with plain JavaScript
- Integrating OpenAI with bounded context, scope controls and usage limits
- Deploying, monitoring and debugging a real application on Linux hardware
- Treating export, deletion, privacy and legal pages as product features
- There are not yet automated pytest regression tests.
- SQL migrations are manually managed; Alembic is a future improvement.
- AI live conditions are currently enriched for only the ten newest locations.
- OpenStreetMap's public tile servers are suitable for small usage, not high traffic commercial workloads.
- The repository needs a fresh English UI demo recording and AI-panel screenshot; no placeholder media is linked.
FrameScout is released under the MIT License.
The application uses third-party services and assets whose own terms still apply, including OpenStreetMap tiles, OpenWeather, Google and OpenAI.
