---
name: "Solventum Image Metadata Tool v3.1"
client: Solventum
status: active
server: ai-sandbox.oliver.solutions
tech: [Python, FastAPI, SQLite, ExifTool, OpenAI GPT-5.2, Docker, Apache]
local_path: /Users/ai_leed/Documents/Projects/Oliver/solventum-image-metadata
deploy: docker-compose up -d OR sudo systemctl restart oliver-metadata
url: https://ai-sandbox.oliver.solutions/solventum-image-metadata/
tags: [solventum, metadata, ai, openai, enterprise]
created: 2026-04-14
port: 5001
db: SQLite
service: oliver-metadata
---

## Overview

Oliver Metadata Tool is an enterprise web application for universal file metadata management and enrichment. Users upload files (300+ formats supported via ExifTool), select a metadata source (Excel lookup, AI generation via OpenAI GPT-5.2, manual entry, or templates), review and edit metadata in the browser, and download processed files with metadata written back. The tool supports PDF, images, Office documents, and video; all user actions are logged to an audit trail. Built and maintained for Solventum (internal Oliver tooling).

## Tech Stack

- **Frontend:** Jinja2 HTML templates, vanilla JavaScript, no framework
- **Backend:** FastAPI (primary, Python 3.11) with legacy Flask support (`web_app.py`)
- **Database:** SQLite (audit log, sessions, config)
- **Infrastructure:** Docker Compose + systemd, Gunicorn + Uvicorn workers, Apache reverse proxy (HTTPS)
- **AI/ML:** OpenAI GPT-5.2 API (Responses v1 endpoint) for metadata generation
- **File processing:** ExifTool (primary), PyPDF, python-docx, Pillow, mutagen (native handlers)
- **Auth:** Microsoft Entra ID SSO (MSAL.js client-side) + local password fallback

## Architecture

The application is a monolithic Python server behind Apache HTTPS proxy. FastAPI (`app/main.py`) is the active production entrypoint, served by Gunicorn with 2 Uvicorn workers on port 5001 (loopback only). All requests to `https://ai-sandbox.oliver.solutions/solventum-image-metadata/` are proxied via Apache to the Docker container.

**Core design pattern:** Every supported file type (PDF, IMAGE, OFFICE_DOC, OFFICE_SHEET, OFFICE_PRESENTATION, VIDEO) has a corresponding **extractor** (read metadata) and **updater** (write metadata) class, all keyed by a `FileType` enum and registered in a shared registry. `FileDetector.detect_file_type(filepath)` routes incoming files by extension, then MIME type as fallback. ExifTool (via PyExifTool) is the primary handler for format coverage; native Python libraries handle specific types directly.

**Processing pipeline:**
1. **Auth** → Login (SSO or local password) routed via `app/routers/auth.py`
2. **Upload** → Files saved to `/app/uploads`, in-memory session created
3. **Metadata source** → User selects: Excel lookup, import from file, AI generation (SSE stream), manual entry, or template
4. **AI generation** → OpenAI GPT-5.2 generates title, subject, keywords per file (async via `app/services/ai_service.py`)
5. **Review** → User edits metadata inline in browser
6. **Write** → Extractor/updater pair writes metadata back to file
7. **Download** → User downloads processed files individually or as ZIP

**Key layers:**
- `app/routers/` — HTTP request handling, validation, response
- `app/services/` — Business logic (AI, auth, file ops, admin)
- `app/session/` — In-memory file-processing session management
- `src/` — Shared library (extractors, updaters, file detection, config, DB, auth)
- `templates/` — Jinja2 HTML (shared by FastAPI and legacy Flask)
- `static/` — CSS, JS, images (served by Apache in production, not Docker)

```
Browser (HTTPS)
  ↓
Apache reverse proxy (ai-sandbox.oliver.solutions:443)
  ├─ /solventum-image-metadata/static → disk (/var/www/html/…)
  └─ /solventum-image-metadata/* → ProxyPass to Docker
      ↓
Docker container oliver-metadata-tool (127.0.0.1:5001)
  ├─ Gunicorn (2 UvicornWorker processes)
  │   └─ FastAPI app (app/main.py)
  │       ├─ Routers (HTTP)
  │       ├─ Services (business logic)
  │       └─ src/ (file ops, DB, auth)
  ├─ SQLite DB (/app/data/oliver_metadata.db)
  ├─ Upload directory (/app/uploads)
  └─ Output directory (/app/output)
```

## Dev Commands

```bash
# Clone and navigate
cd /Users/ai_leed/Documents/Projects/Oliver/solventum-image-metadata

# Create virtual environment
python3 -m venv venv_local
source venv_local/bin/activate

# Install dependencies
pip install -r requirements.txt

# Install system dependencies (macOS)
brew install exiftool tesseract tesseract-lang poppler

# Verify ExifTool (must be 12.15+)
exiftool -ver

# Configure environment
cp .env.example .env  # or create .env manually
# Edit .env and set OPENAI_API_KEY, AZURE_CLIENT_ID, etc.

# Run FastAPI (production entrypoint)
gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 1 --bind 127.0.0.1:5001 --reload

# Or run Flask legacy (simpler for local dev)
python web_app.py

# Open browser
# http://localhost:5001
# Test user: tester / oliveradmin (if ENABLE_TEST_USER=true)

# Docker (build and start)
docker-compose up -d

# View logs
docker-compose logs -f oliver-metadata

# Stop
docker-compose down
```

## Deployment

- **Server:** ai-sandbox.oliver.solutions (Ubuntu/Debian, Apache 2.4, Docker)
- **Deploy:** `docker-compose build --no-cache oliver-metadata && docker-compose up -d` OR `sudo systemctl restart oliver-metadata`
- **URL:** https://ai-sandbox.oliver.solutions/solventum-image-metadata/
- **Port:** 5001 (internal Docker container, loopback only; Apache proxies externally on 443)
- **Service:** `oliver-metadata` (systemd unit file: `deploy/oliver-metadata.service`)
- **Local path:** /Users/ai_leed/Documents/Projects/Oliver/solventum-image-metadata
- **Deploy path (server):** `/var/www/oliver/`
- **Static files (server):** `/var/www/html/solventum-image-metadata/static/`

**Process manager:** Gunicorn + Uvicorn workers (2 workers, 120s timeout). Health check: `curl -sf http://localhost:5001/login` every 30s.

**Docker volumes:**
- `uploads` → `/app/uploads` (temp uploaded files)
- `database` → `/app/data` (SQLite: `oliver_metadata.db`, `oliver_sessions.db`)
- `output` → `/app/output` (processed files, backups, reports, templates)

## Environment Variables

| Variable | Purpose |
|----------|---------|
| `OPENAI_API_KEY` | OpenAI API key for GPT-5.2 metadata generation |
| `AZURE_CLIENT_ID` | Microsoft Entra ID (Azure AD) client ID for SSO (MSAL.js) |
| `AZURE_TENANT_ID` | Microsoft Entra ID tenant ID for SSO |
| `AZURE_CLIENT_SECRET` | Optional; client secret for server-side token exchange (MSAL.js is client-side, so this is optional) |
| `ENABLE_TEST_USER` | Enable test login `tester / oliveradmin` (dev/staging only) |
| `DATABASE_URL` | SQLite connection string (default: `sqlite:///./data/oliver_metadata.db`) |
| `SESSION_STORE` | Session persistence backend (`memory` or `sqlite`) |
| `LOG_LEVEL` | Python logging level (`INFO`, `DEBUG`, `WARNING`) |
| `ADMIN_EMAILS` | Comma-separated list of superadmin email addresses |
| `FILE_UPLOAD_MAX_MB` | Max file size in MB (default: 500) |
| `AI_TIMEOUT_SECONDS` | OpenAI API timeout (default: 60) |

## API / Endpoints

All endpoints routed via `app/routers/`:

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/login` | GET, POST | SSO (MSAL.js) or local password auth |
| `/logout` | POST | Destroy session |
| `/upload` | POST | Receive file upload, create session |
| `/metadata/extract` | POST | Extract existing metadata from uploaded file |
| `/metadata/source/excel` | POST | Lookup metadata from pre-loaded Excel |
| `/metadata/source/import` | POST | Import metadata from CSV/Excel/JSON with column mapping |
| `/metadata/source/ai` | POST | Stream AI-generated metadata via SSE |
| `/metadata/update` | POST | Write metadata back to file(s) |
| `/download` | GET | Download single file or ZIP of processed files |
| `/admin/audit` | GET | Audit log (superadmin only) |
| `/admin/users` | GET, POST, DELETE | User management (superadmin only) |

## Known Issues

- **Azure AD SSO:** Last fix (commit `0976ee9`) addressed `AADSTS900144` client_id validation error; verify `AZURE_CLIENT_ID` matches Azure app registration
- **Filename collisions:** Fixed in commit `1156209`; Excel metadata lookup now handles duplicate filenames
- **AI bulk uploads:** Background processing supports up to 100 files; larger batches should be chunked
-

## Sessions
### 2026-04-14 – Project catalogued
**Done:** Added to Obsidian second brain.

---
## Change Log
| Date | Requested | Changed | Files |
|------|-----------|---------|-------|
| 2026-04-14 | Initial setup | Note created | — |