docs: add VitePress documentation site with GitHub Pages deployment

Rewrites all documentation with accurate project details (Fastify, port
1349, single-container Docker, all 33+ tools, full database schema).
Adds getting started guide and configuration reference. Updates help and
settings dialogs to link to the docs site.
This commit is contained in:
Siddharth Kumar Sah
2026-03-22 21:00:37 +08:00
parent 977b5f5ec0
commit 6668615750
16 changed files with 1457 additions and 6 deletions
+58
View File
@@ -0,0 +1,58 @@
name: AI Documentation Updater
on:
push:
branches:
- main
paths:
- 'apps/api/**'
- 'apps/web/**'
- 'packages/**'
jobs:
update-docs:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2 # Fetch previous commit to get diff
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Generate Diff
run: git diff HEAD^ HEAD > diff.patch
- name: Run Real AI Agent
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
echo "Running Claude to analyze changes and update docs..."
npx tsx apps/docs/scripts/update-docs.ts ./diff.patch
- name: Commit and Push Changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add apps/docs/
if ! git diff-index --quiet HEAD; then
git commit -m "docs: auto-updated by AI 🤖"
git push
else
echo "No documentation changes needed."
fi
+55
View File
@@ -0,0 +1,55 @@
name: Deploy Docs to GitHub Pages
on:
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build Docs
run: pnpm --filter @stirling-image/docs docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: apps/docs/.vitepress/dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+59
View File
@@ -0,0 +1,59 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
title: "Stirling Image",
description: "Documentation for Stirling Image, a self-hosted image processing suite.",
base: '/Stirling-Image/',
srcDir: '.',
outDir: './.vitepress/dist',
head: [
['meta', { name: 'theme-color', content: '#3b82f6' }],
],
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/getting-started' },
{ text: 'API Reference', link: '/api/rest' }
],
sidebar: [
{
text: 'Guide',
items: [
{ text: 'Getting started', link: '/guide/getting-started' },
{ text: 'Architecture', link: '/guide/architecture' },
{ text: 'Configuration', link: '/guide/configuration' },
{ text: 'Database', link: '/guide/database' },
{ text: 'Deployment', link: '/guide/deployment' }
]
},
{
text: 'API reference',
items: [
{ text: 'REST API', link: '/api/rest' },
{ text: 'Image engine', link: '/api/image-engine' },
{ text: 'AI engine', link: '/api/ai' }
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/siddharthksah/Stirling-Image' }
],
search: {
provider: 'local'
},
footer: {
message: 'Released under the MIT License.',
},
editLink: {
pattern: 'https://github.com/siddharthksah/Stirling-Image/edit/main/apps/docs/:path',
text: 'Edit this page on GitHub'
}
}
})
+95
View File
@@ -0,0 +1,95 @@
# AI engine
The `@stirling-image/ai` package wraps Python ML models in TypeScript functions. Each operation spawns a Python subprocess, processes the image, and returns the result. The bridge layer handles serialization and error propagation.
All model weights are bundled in the Docker image during the build. No downloads happen at runtime.
## Background removal
Removes the background from an image and returns a transparent PNG.
**Model:** BiRefNet-Lite via [rembg](https://github.com/danielgatis/rembg)
| Parameter | Type | Description |
|---|---|---|
| `model` | string | Model name. Default: `birefnet-lite`. Options include `u2net`, `isnet-general-use`, and others supported by rembg. |
| `alphaMatting` | boolean | Use alpha matting for finer edge detail |
| `alphaMattingForegroundThreshold` | number | Foreground threshold for alpha matting (0-255) |
| `alphaMattingBackgroundThreshold` | number | Background threshold for alpha matting (0-255) |
**Python script:** `packages/ai/python/remove_bg.py`
## Upscaling
Increases image resolution using AI super-resolution.
**Model:** [RealESRGAN](https://github.com/xinntao/Real-ESRGAN)
| Parameter | Type | Description |
|---|---|---|
| `scale` | number | Upscale factor: `2` or `4` |
Returns the upscaled image along with the original and new dimensions.
**Python script:** `packages/ai/python/upscale.py`
## OCR (text recognition)
Extracts text from images.
**Model:** [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)
| Parameter | Type | Description |
|---|---|---|
| `language` | string | Language code (e.g. `en`, `ch`, `fr`, `de`) |
Returns structured results with text content, bounding boxes, and confidence scores for each detected text region.
**Python script:** `packages/ai/python/ocr.py`
## Face detection and blurring
Detects faces in an image and applies a blur to each detected region.
**Model:** [MediaPipe](https://github.com/google/mediapipe) Face Detection
| Parameter | Type | Description |
|---|---|---|
| `blurStrength` | number | How strongly to blur detected faces |
Returns the blurred image along with metadata about each detected face region (bounding box coordinates and confidence score).
**Python script:** `packages/ai/python/detect_faces.py`
## Object erasing (inpainting)
Removes objects from images by filling in the area with generated content that matches the surroundings.
**Model:** [LaMa](https://github.com/advimman/lama) (Large Mask Inpainting)
Takes an image and a mask (white = area to erase, black = keep). Returns the inpainted image.
**Python script:** `packages/ai/python/inpaint.py`
## Smart crop
Content-aware cropping that identifies the most relevant region of an image.
| Parameter | Type | Description |
|---|---|---|
| `width` | number | Target crop width |
| `height` | number | Target crop height |
Unlike regular cropping, smart crop analyzes the image content to decide where to place the crop window.
## How the bridge works
The TypeScript bridge (`packages/ai/src/bridge.ts`) does the following for each AI call:
1. Writes the input image to a temp file in the workspace directory.
2. Spawns a Python subprocess with the appropriate script and arguments.
3. Reads stdout for JSON output and stderr for error messages.
4. Reads the output image from the filesystem.
5. Cleans up temp files.
If the Python process exits with a non-zero code or writes to stderr, the bridge throws an error with the stderr content. Timeouts are handled at the API route level.
+122
View File
@@ -0,0 +1,122 @@
# Image engine
The `@stirling-image/image-engine` package handles all non-AI image operations. It wraps [Sharp](https://sharp.pixelplumbing.com/) and runs entirely in-process with no external dependencies.
## Operations
### resize
Scale an image to specific dimensions or by percentage.
| Parameter | Type | Description |
|---|---|---|
| `width` | number | Target width in pixels |
| `height` | number | Target height in pixels |
| `fit` | string | `cover`, `contain`, `fill`, `inside`, or `outside` |
| `withoutEnlargement` | boolean | If true, won't upscale smaller images |
| `percentage` | number | Scale by percentage instead of absolute dimensions |
You can set `width`, `height`, or both. If you only set one, the other is calculated to maintain the aspect ratio.
### crop
Cut out a rectangular region from the image.
| Parameter | Type | Description |
|---|---|---|
| `left` | number | X offset from the left edge |
| `top` | number | Y offset from the top edge |
| `width` | number | Width of the crop area |
| `height` | number | Height of the crop area |
### rotate
Rotate the image by a given angle.
| Parameter | Type | Description |
|---|---|---|
| `angle` | number | Rotation angle in degrees (0-360) |
| `background` | string | Fill color for the exposed area (default: transparent or white) |
### flip
Mirror the image horizontally or vertically.
| Parameter | Type | Description |
|---|---|---|
| `direction` | string | `horizontal` or `vertical` |
### convert
Change the image format.
| Parameter | Type | Description |
|---|---|---|
| `format` | string | Target format: `jpeg`, `png`, `webp`, `avif`, `tiff`, `gif` |
| `quality` | number | Compression quality (1-100, applies to lossy formats) |
### compress
Reduce file size while keeping the same format.
| Parameter | Type | Description |
|---|---|---|
| `quality` | number | Target quality (1-100) |
| `format` | string | Optional format override |
### strip-metadata
Remove EXIF, IPTC, and XMP metadata from the image. Useful for privacy before sharing photos publicly. Takes no parameters.
### Color adjustments
These operations modify the color properties of an image. Each takes a single numeric value.
| Operation | Parameter | Range | Description |
|---|---|---|---|
| `brightness` | `value` | -100 to 100 | Adjust brightness |
| `contrast` | `value` | -100 to 100 | Adjust contrast |
| `saturation` | `value` | -100 to 100 | Adjust color saturation |
### Color filters
These apply a fixed color transformation. They take no parameters.
| Operation | Description |
|---|---|
| `grayscale` | Convert to grayscale |
| `sepia` | Apply a sepia tone |
| `invert` | Invert all colors |
### Color channels
Adjust individual RGB color channels.
| Parameter | Type | Description |
|---|---|---|
| `red` | number | Red channel adjustment (-100 to 100) |
| `green` | number | Green channel adjustment (-100 to 100) |
| `blue` | number | Blue channel adjustment (-100 to 100) |
## Format detection
The engine detects input formats automatically from file headers, not just file extensions. This means a `.jpg` file that is actually a PNG will be handled correctly.
Supported input formats: JPEG, PNG, WebP, AVIF, TIFF, GIF, SVG, RAW (via libraw).
## Metadata extraction
The `info` tool returns image metadata:
```json
{
"width": 1920,
"height": 1080,
"format": "jpeg",
"size": 245678,
"channels": 3,
"hasAlpha": false,
"dpi": 72,
"exif": { ... }
}
```
+285
View File
@@ -0,0 +1,285 @@
# REST API
The API server runs on port 1349 by default and serves all endpoints under `/api`. Interactive Swagger documentation is available at `/api/docs` when the server is running.
## Authentication
Requests can be authenticated in two ways:
1. **Session cookie** -- Log in via `POST /api/auth/login` and the server sets a session cookie.
2. **API key** -- Pass an `Authorization: Bearer si_...` header with an API key.
Some endpoints (health check, login, Swagger docs) are public and don't require authentication.
## Tools
### Execute a tool
```
POST /api/v1/tools/:toolId
Content-Type: multipart/form-data
```
Send a multipart request with:
- `file` -- The image file
- `settings` -- JSON string with tool-specific options
Response:
```json
{
"jobId": "abc123",
"downloadUrl": "/api/v1/download/abc123/output.png",
"originalSize": 245000,
"processedSize": 180000
}
```
### Available tool IDs
**Image operations:** `resize`, `crop`, `rotate`, `flip`, `convert`, `compress`, `strip-metadata`, `border`
**Color:** `color-adjustments`, `grayscale`, `sepia`, `invert`, `color-palette`, `replace-color`
**Text and codes:** `watermark-text`, `watermark-image`, `text-overlay`, `qr-generate`, `barcode-read`, `ocr`
**Composition:** `compose`, `collage`, `split`, `image-to-pdf`
**Analysis:** `info`, `compare`, `find-duplicates`
**Conversion:** `svg-to-raster`, `vectorize`, `favicon`, `gif-tools`
**AI-powered:** `remove-background`, `upscale`, `blur-faces`, `erase-object`, `smart-crop`
**Utility:** `bulk-rename`
### Batch processing
```
POST /api/v1/tools/:toolId/batch
Content-Type: multipart/form-data
```
Send multiple files with the same settings. Returns a ZIP file containing all processed images. The response includes an `X-Job-Id` header you can use to track progress.
## File management
### Upload files
```
POST /api/v1/upload
Content-Type: multipart/form-data
```
Upload one or more images. Returns file identifiers for use with other endpoints.
### Download results
```
GET /api/v1/download/:jobId/:filename
```
Download a processed image by job ID and filename.
## Pipelines
Pipelines chain multiple tools together. The output of each step becomes the input for the next.
### Execute a pipeline
```
POST /api/v1/pipeline/execute
Content-Type: multipart/form-data
```
Body fields:
- `file` -- The input image
- `steps` -- JSON array of `{ "toolId": "resize", "settings": { ... } }` objects
Response includes `jobId`, `downloadUrl`, and details about each completed step.
### Save a pipeline
```
POST /api/v1/pipeline/save
Content-Type: application/json
```
```json
{
"name": "Thumbnail generator",
"description": "Resize and compress for web thumbnails",
"steps": [
{ "toolId": "resize", "settings": { "width": 200, "height": 200, "fit": "cover" } },
{ "toolId": "compress", "settings": { "quality": 80 } },
{ "toolId": "convert", "settings": { "format": "webp" } }
]
}
```
### List saved pipelines
```
GET /api/v1/pipeline/list
```
### Delete a pipeline
```
DELETE /api/v1/pipeline/:id
```
## Progress tracking
For long-running jobs (AI operations, batch processing), you can track progress via Server-Sent Events.
```
GET /api/v1/jobs/:jobId/progress
```
The stream emits `JobProgress` objects:
```json
{
"status": "processing",
"progress": 45,
"completedFiles": ["image1.jpg", "image2.jpg"],
"failedFiles": [],
"errors": []
}
```
The connection closes automatically 5 seconds after the job completes.
## API keys
### Generate a key
```
POST /api/v1/api-keys
Content-Type: application/json
```
```json
{ "name": "My integration" }
```
Returns the raw key (prefixed with `si_`). This is the only time the full key is shown.
### List keys
```
GET /api/v1/api-keys
```
Returns key metadata (name, prefix, creation date) but not the full key.
### Delete a key
```
DELETE /api/v1/api-keys/:id
```
## Settings
### Get all settings
```
GET /api/v1/settings
```
### Update settings
```
PUT /api/v1/settings
Content-Type: application/json
```
Admin only. Accepts a JSON object of key-value pairs.
## Auth endpoints
### Login
```
POST /api/auth/login
Content-Type: application/json
```
```json
{ "username": "admin", "password": "admin" }
```
Returns a session token and sets a cookie.
### Get current session
```
GET /api/auth/session
```
### Change password
```
POST /api/auth/change-password
Content-Type: application/json
```
```json
{ "currentPassword": "old", "newPassword": "new" }
```
### List users (admin)
```
GET /api/auth/users
```
### Create user (admin)
```
POST /api/auth/register
Content-Type: application/json
```
```json
{ "username": "newuser", "password": "pass", "role": "user" }
```
### Delete user (admin)
```
DELETE /api/auth/users/:id
```
## Health check
```
GET /api/v1/health
```
Returns `200 OK` if the server is running. Used by Docker's health check.
## Rate limiting
All endpoints are rate-limited to `RATE_LIMIT_PER_MIN` requests per minute per IP (default: 100). When exceeded, the server returns `429 Too Many Requests`.
## Error responses
Errors follow a consistent format:
```json
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid image format"
}
```
Common status codes:
- `400` -- Invalid input (bad format, missing required fields)
- `401` -- Not authenticated
- `403` -- Not authorized (e.g., non-admin trying admin endpoints)
- `413` -- File too large (exceeds `MAX_UPLOAD_SIZE_MB`)
- `429` -- Rate limited
- `500` -- Server error
+82
View File
@@ -0,0 +1,82 @@
# Architecture
Stirling Image is a monorepo managed with pnpm workspaces and Turborepo. Everything ships as a single Docker container.
## Project structure
```
Stirling-Image/
├── apps/
│ ├── api/ # Fastify backend
│ ├── web/ # React + Vite frontend
│ └── docs/ # This VitePress site
├── packages/
│ ├── image-engine/ # Sharp-based image operations
│ ├── ai/ # Python AI model bridge
│ └── shared/ # Types, constants, i18n
└── docker/ # Dockerfile and Compose config
```
## Packages
### `@stirling-image/image-engine`
The core image processing library built on [Sharp](https://sharp.pixelplumbing.com/). It handles all non-AI operations: resize, crop, rotate, flip, convert, compress, strip metadata, and color adjustments (brightness, contrast, saturation, grayscale, sepia, invert, color channels).
This package has no network dependencies and runs entirely in-process.
### `@stirling-image/ai`
A bridge layer that calls Python scripts via child processes. Each AI capability has a TypeScript wrapper that spawns a Python subprocess, passes image data through the filesystem, and returns the result.
Supported operations:
- **Background removal** -- BiRefNet-Lite model via rembg
- **Upscaling** -- RealESRGAN
- **OCR** -- PaddleOCR
- **Face detection/blurring** -- MediaPipe
- **Object erasing (inpainting)** -- LaMa Cleaner
- **Smart crop** -- content-aware cropping
Python scripts live in `packages/ai/python/`. The Docker image pre-downloads all model weights during the build so the container works offline.
### `@stirling-image/shared`
Shared TypeScript types, constants (like `APP_VERSION` and tool definitions), and i18n translation strings used by both the frontend and backend.
## Applications
### API (`apps/api`)
A Fastify v5 server that handles:
- File uploads and temporary workspace management
- Tool execution (routes each tool request to the image engine or AI bridge)
- Pipeline orchestration (chaining multiple tools sequentially)
- Batch processing with concurrency control via p-queue
- User authentication, API key management, and rate limiting
- Swagger/OpenAPI documentation at `/api/docs`
- Serving the built frontend as a SPA in production
Key dependencies: Fastify, Drizzle ORM, better-sqlite3, Sharp, Zod for validation.
### Web (`apps/web`)
A React 19 single-page app built with Vite. Uses Zustand for state management, Tailwind CSS v4 for styling, and Lucide for icons. Communicates with the API over REST and SSE (for progress tracking).
The built frontend gets served by the Fastify backend in production, so there is no separate web server in the Docker container.
### Docs (`apps/docs`)
This VitePress site. Deployed to GitHub Pages automatically on push to `main`.
## How a request flows
1. The user picks a tool in the web UI and uploads an image.
2. The frontend sends a multipart POST to `/api/v1/tools/:toolId` with the file and settings.
3. The API route validates the input with Zod, then calls the appropriate package function -- either `@stirling-image/image-engine` for standard operations or `@stirling-image/ai` for ML tasks.
4. For AI tools, the TypeScript bridge spawns a Python subprocess, waits for it to finish, and reads the output file.
5. The API returns a `jobId` and `downloadUrl`. The frontend can poll `/api/v1/jobs/:jobId/progress` via SSE for real time status on longer tasks.
6. The user downloads the processed image from `/api/v1/download/:jobId/:filename`.
For pipelines, the API feeds the output of each step as input to the next, running them sequentially.
For batch processing, the API uses p-queue with a configurable concurrency limit (`CONCURRENT_JOBS`) and returns a ZIP file with all processed images.
+80
View File
@@ -0,0 +1,80 @@
# Configuration
All configuration is done through environment variables. Every variable has a sensible default, so Stirling Image works out of the box without setting any of them.
## Environment variables
### Server
| Variable | Default | Description |
|---|---|---|
| `PORT` | `1350` | Port the server listens on. The Docker image overrides this to `1349`. |
| `RATE_LIMIT_PER_MIN` | `100` | Maximum requests per minute per IP. |
### Authentication
| Variable | Default | Description |
|---|---|---|
| `AUTH_ENABLED` | `false` | Set to `true` to require login. The Docker image defaults to `true`. |
| `DEFAULT_USERNAME` | `admin` | Username for the initial admin account. Only used on first run. |
| `DEFAULT_PASSWORD` | `admin` | Password for the initial admin account. Change this after first login. |
### Storage
| Variable | Default | Description |
|---|---|---|
| `STORAGE_MODE` | `local` | `local` or `s3`. Only local storage is currently implemented. |
| `DB_PATH` | `./data/stirling.db` | Path to the SQLite database file. |
| `WORKSPACE_PATH` | `./tmp/workspace` | Directory for temporary files during processing. Cleaned up automatically. |
### Processing limits
| Variable | Default | Description |
|---|---|---|
| `MAX_UPLOAD_SIZE_MB` | `100` | Maximum file size per upload in megabytes. |
| `MAX_BATCH_SIZE` | `200` | Maximum number of files in a single batch request. |
| `CONCURRENT_JOBS` | `3` | Number of batch jobs that run in parallel. Higher values use more memory. |
| `MAX_MEGAPIXELS` | `100` | Maximum image resolution allowed. Rejects images larger than this. |
### Cleanup
| Variable | Default | Description |
|---|---|---|
| `FILE_MAX_AGE_HOURS` | `24` | How long temporary files are kept before automatic deletion. |
| `CLEANUP_INTERVAL_MINUTES` | `30` | How often the cleanup job runs. |
### Appearance
| Variable | Default | Description |
|---|---|---|
| `APP_NAME` | `Stirling Image` | Display name shown in the UI. |
| `DEFAULT_THEME` | `light` | Default theme for new sessions. `light` or `dark`. |
| `DEFAULT_LOCALE` | `en` | Default interface language. |
## Docker example
```yaml
services:
stirling-image:
image: siddharth123sk/stirling-image:latest
ports:
- "1349:1349"
volumes:
- stirling-data:/data
- stirling-workspace:/tmp/workspace
environment:
- AUTH_ENABLED=true
- DEFAULT_USERNAME=admin
- DEFAULT_PASSWORD=changeme
- MAX_UPLOAD_SIZE_MB=200
- CONCURRENT_JOBS=4
- FILE_MAX_AGE_HOURS=12
restart: unless-stopped
```
## Volumes
The Docker container uses two volumes:
- `/data` -- Persistent storage for the SQLite database. Mount this to keep users, API keys, and saved pipelines across container restarts.
- `/tmp/workspace` -- Temporary storage for images being processed. This can be ephemeral, but mounting it avoids filling up the container's writable layer.
+96
View File
@@ -0,0 +1,96 @@
# Database
Stirling Image uses SQLite with [Drizzle ORM](https://orm.drizzle.team/) for data persistence. The schema is defined in `apps/api/src/db/schema.ts`.
The database file lives at the path set by `DB_PATH` (defaults to `./data/stirling.db`). In Docker, mount the `/data` volume to persist it across container restarts.
## Tables
### users
Stores user accounts. Created automatically on first run from `DEFAULT_USERNAME` and `DEFAULT_PASSWORD`.
| Column | Type | Notes |
|---|---|---|
| `id` | integer | Primary key, auto-increment |
| `username` | text | Unique, required |
| `passwordHash` | text | bcrypt hash |
| `role` | text | `admin` or `user` |
| `mustChangePassword` | integer | Boolean flag for forced password reset |
| `createdAt` | text | ISO timestamp |
| `updatedAt` | text | ISO timestamp |
### sessions
Active login sessions. Each row ties a session token to a user.
| Column | Type | Notes |
|---|---|---|
| `id` | text | Primary key (session token) |
| `userId` | integer | Foreign key to `users.id` |
| `expiresAt` | text | ISO timestamp |
| `createdAt` | text | ISO timestamp |
### api_keys
API keys for programmatic access. The raw key is shown once on creation; only the hash is stored.
| Column | Type | Notes |
|---|---|---|
| `id` | integer | Primary key, auto-increment |
| `userId` | integer | Foreign key to `users.id` |
| `keyHash` | text | SHA-256 hash of the key |
| `name` | text | User-provided label |
| `createdAt` | text | ISO timestamp |
| `lastUsedAt` | text | Updated on each authenticated request |
Keys are prefixed with `si_` followed by 96 hex characters (48 random bytes).
### pipelines
Saved tool chains that users create in the UI.
| Column | Type | Notes |
|---|---|---|
| `id` | integer | Primary key, auto-increment |
| `name` | text | Pipeline name |
| `description` | text | Optional description |
| `steps` | text | JSON array of `{ toolId, settings }` objects |
| `createdAt` | text | ISO timestamp |
### jobs
Tracks processing jobs for progress reporting and cleanup.
| Column | Type | Notes |
|---|---|---|
| `id` | text | Primary key (UUID) |
| `type` | text | Tool or pipeline identifier |
| `status` | text | `queued`, `processing`, `completed`, or `failed` |
| `progress` | integer | 0-100 percentage |
| `inputFiles` | text | JSON array of input file paths |
| `outputPath` | text | Path to the result file |
| `settings` | text | JSON of the tool settings used |
| `error` | text | Error message if failed |
| `createdAt` | text | ISO timestamp |
| `completedAt` | text | ISO timestamp |
### settings
Key-value store for server-wide settings that admins can change from the UI.
| Column | Type | Notes |
|---|---|---|
| `key` | text | Primary key |
| `value` | text | Setting value |
| `updatedAt` | text | ISO timestamp |
## Migrations
Drizzle handles schema migrations. The config is in `apps/api/drizzle.config.ts`. During development, run:
```bash
pnpm --filter @stirling-image/api drizzle-kit push
```
In production, the schema is applied automatically on startup.
+112
View File
@@ -0,0 +1,112 @@
# Deployment
Stirling Image ships as a single Docker container. The frontend, API, and Python AI runtime all run inside one image.
## Docker Compose (recommended)
```yaml
services:
stirling-image:
image: siddharth123sk/stirling-image:latest
container_name: stirling-image
ports:
- "1349:1349"
volumes:
- stirling-data:/data
- stirling-workspace:/tmp/workspace
environment:
- AUTH_ENABLED=true
- DEFAULT_USERNAME=admin
- DEFAULT_PASSWORD=admin
restart: unless-stopped
volumes:
stirling-data:
stirling-workspace:
```
```bash
docker compose up -d
```
The app is then available at `http://localhost:1349`.
## What's inside the container
The Docker image uses a multi-stage build:
1. **Build stage** -- Installs Node.js dependencies and builds the React frontend with Vite.
2. **Production stage** -- Copies the built frontend and API source into a Node 22 image, installs system dependencies (Python 3, ImageMagick, Tesseract, potrace), sets up a Python virtual environment with all ML packages, and pre-downloads model weights.
Everything runs from a single process. The Fastify server handles API requests and serves the frontend SPA.
### System dependencies installed in the image
- Python 3 with pip
- ImageMagick
- Tesseract OCR
- libraw (RAW image support)
- potrace (bitmap to vector conversion)
### Python packages
- rembg with BiRefNet-Lite (background removal)
- RealESRGAN (upscaling)
- PaddleOCR (text recognition)
- MediaPipe (face detection)
- LaMa Cleaner (inpainting/object removal)
- onnxruntime, opencv-python, Pillow, numpy
Model weights are downloaded at build time, so the container works fully offline.
## Volumes
Mount these to persist data:
| Mount point | Purpose |
|---|---|
| `/data` | SQLite database (users, API keys, pipelines, settings) |
| `/tmp/workspace` | Temporary image processing files |
The `/data` volume is the important one. Without it, you lose all user accounts and saved pipelines on container restart. The workspace volume is optional but prevents the container's writable layer from growing.
## Health check
The container includes a health check that hits `GET /api/v1/health`. Docker uses this to report container status:
```bash
docker inspect --format='{{.State.Health.Status}}' stirling-image
```
## Reverse proxy
If you're running Stirling Image behind nginx or Caddy, point it at port 1349. Example nginx config:
```nginx
server {
listen 80;
server_name images.example.com;
client_max_body_size 200M;
location / {
proxy_pass http://localhost:1349;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
Set `client_max_body_size` to match your `MAX_UPLOAD_SIZE_MB` value.
## CI/CD
The GitHub repository has two workflows:
- **docker-publish.yml** -- Builds and pushes the Docker image to Docker Hub on every push to `main` and on version tags. The image is published as `siddharth123sk/stirling-image`.
- **deploy-docs.yml** -- Builds this documentation site and deploys it to GitHub Pages.
Both run automatically. No manual steps needed after merging to `main`.
+78
View File
@@ -0,0 +1,78 @@
# Getting started
## Run with Docker
The fastest way to get Stirling Image running:
```bash
docker run -d \
--name stirling-image \
-p 1349:1349 \
-v stirling-data:/data \
siddharth123sk/stirling-image:latest
```
Open `http://localhost:1349` in your browser. Log in with `admin` / `admin`.
## Run with Docker Compose
Create a `docker-compose.yml`:
```yaml
services:
stirling-image:
image: siddharth123sk/stirling-image:latest
container_name: stirling-image
ports:
- "1349:1349"
volumes:
- stirling-data:/data
- stirling-workspace:/tmp/workspace
environment:
- AUTH_ENABLED=true
- DEFAULT_USERNAME=admin
- DEFAULT_PASSWORD=admin
restart: unless-stopped
volumes:
stirling-data:
stirling-workspace:
```
```bash
docker compose up -d
```
See [Configuration](./configuration) for the full list of environment variables.
## Build from source
Requirements: Node.js 20+, pnpm 9+, Python 3.10+
```bash
git clone https://github.com/siddharthksah/Stirling-Image.git
cd Stirling-Image
pnpm install
```
Start the dev server:
```bash
pnpm dev
```
This starts both the API server and the React frontend. The app opens at `http://localhost:5173` by default during development.
## What you can do
Once logged in, the sidebar lists every available tool. Pick one, upload an image, adjust the settings, and download the result.
A few things to try first:
- **Resize** an image to specific dimensions or a percentage
- **Remove a background** using the AI-powered background removal tool
- **Compress** a photo to reduce file size before uploading it somewhere
- **Convert** between formats (JPEG, PNG, WebP, AVIF, TIFF)
- **Batch process** a folder of images through any tool
Every tool in the UI is also available through the [REST API](../api/rest), so you can script your workflows or integrate Stirling Image into other systems.
+25
View File
@@ -0,0 +1,25 @@
---
layout: home
hero:
name: "Stirling Image"
text: "Self-hosted image processing"
tagline: Resize, compress, convert, remove backgrounds, and more. All on your own server, no data leaves your machine.
actions:
- theme: brand
text: Get started
link: /guide/getting-started
- theme: alt
text: API reference
link: /api/rest
features:
- title: 33+ tools
details: Resize, crop, compress, convert, watermark, OCR, background removal, upscaling, face blurring, and more.
- title: Runs locally
details: Single Docker container. Your images stay on your server, nothing gets sent to external services.
- title: REST API
details: Every tool is available through the API. Upload a file, pick a tool, get your result back.
- title: Pipeline support
details: Chain multiple tools together and save them as reusable pipelines. Batch process entire folders.
---
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@stirling-image/docs",
"version": "0.1.0",
"private": true,
"scripts": {
"docs:dev": "vitepress dev .",
"docs:build": "vitepress build .",
"docs:preview": "vitepress preview ."
},
"devDependencies": {
"tsx": "^4.19.0",
"vitepress": "^1.1.4"
}
}
+123
View File
@@ -0,0 +1,123 @@
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
import path from 'path';
// Recursively get all markdown files
function getMdFiles(dir: string, fileList: string[] = []): string[] {
const files = readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
if (statSync(filePath).isDirectory()) {
if (file !== 'node_modules' && file !== '.vitepress' && file !== 'scripts') {
getMdFiles(filePath, fileList);
}
} else if (file.endsWith('.md')) {
fileList.push(filePath);
}
}
return fileList;
}
async function main() {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error("ANTHROPIC_API_KEY environment variable is missing.");
process.exit(1);
}
const diffPath = process.argv[2];
if (!diffPath) {
console.error("Please provide the path to the diff file. Usage: npx tsx apps/docs/scripts/update-docs.ts <path-to-diff>");
process.exit(1);
}
const diffContent = readFileSync(path.resolve(process.cwd(), diffPath), 'utf-8');
if (!diffContent.trim()) {
console.log("No diff content. Exiting.");
return;
}
const docsDir = path.resolve(__dirname, '..');
const mdFiles = getMdFiles(docsDir);
const docsContext = mdFiles.map(file => {
const relativePath = path.relative(docsDir, file);
const content = readFileSync(file, 'utf-8');
return `--- FILE: ${relativePath} ---\n${content}\n`;
}).join('\n');
const prompt = `You are an expert technical writer and developer maintaining documentation for a project.
A code change has just been merged.
Here is the git diff of the code changes:
\`\`\`diff
${diffContent}
\`\`\`
Here is the current VitePress documentation (Markdown files):
${docsContext}
Task:
Analyze the git diff and determine if any of the documentation files need to be updated to reflect these code changes.
If updates are needed, output a JSON array of objects with 'file' and 'content' properties.
- 'file' MUST be the exact relative path of the file to update (e.g., 'guide/architecture.md').
- 'content' MUST be the complete, updated markdown content for that file.
If no updates are needed, output an empty array: []
IMPORTANT: Respond ONLY with valid JSON. Do not include markdown formatting like \`\`\`json around your response. Just the raw JSON array.`;
console.log("Sending diff and current docs to Claude API...");
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20241022",
max_tokens: 4096,
messages: [{ role: "user", content: prompt }]
})
});
if (!response.ok) {
const errorText = await response.text();
console.error(\`API Error (\${response.status}):\`, errorText);
process.exit(1);
}
const data = await response.json();
const responseText = data.content?.[0]?.text;
if (!responseText) {
console.log("No response text from AI.");
return;
}
try {
const updates = JSON.parse(responseText);
if (!Array.isArray(updates) || updates.length === 0) {
console.log("AI determined no documentation updates are needed.");
return;
}
for (const update of updates) {
if (update.file && update.content) {
const fullPath = path.join(docsDir, update.file);
writeFileSync(fullPath, update.content, 'utf-8');
console.log(\`Successfully updated \${update.file}\`);
}
}
console.log("Documentation update complete.");
} catch (e) {
console.error("Failed to parse AI response as JSON", e);
console.log("Raw Response:", responseText);
process.exit(1);
}
}
main().catch(err => {
console.error("An unexpected error occurred:", err);
process.exit(1);
});
@@ -0,0 +1,159 @@
import { useEffect } from "react";
import { X, Keyboard, BookOpen, Github, ExternalLink } from "lucide-react";
import { APP_VERSION } from "@stirling-image/shared";
import { formatShortcut } from "@/hooks/use-keyboard-shortcuts";
interface HelpDialogProps {
open: boolean;
onClose: () => void;
}
const SHORTCUTS = [
{ keys: "mod+k", description: "Focus search bar" },
{ keys: "mod+/", description: "Go to tools" },
{ keys: "mod+shift+d", description: "Toggle theme" },
{ keys: "mod+alt+1", description: "Go to Resize" },
{ keys: "mod+alt+2", description: "Go to Crop" },
{ keys: "mod+alt+3", description: "Go to Compress" },
{ keys: "mod+alt+4", description: "Go to Convert" },
{ keys: "mod+alt+5", description: "Go to Remove Background" },
{ keys: "mod+alt+6", description: "Go to Watermark Text" },
{ keys: "mod+alt+7", description: "Go to Strip Metadata" },
{ keys: "mod+alt+8", description: "Go to Image Info" },
];
export function HelpDialog({ open, onClose }: HelpDialogProps) {
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, onClose]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
<div className="relative bg-background border border-border rounded-xl shadow-2xl w-full max-w-lg max-h-[85vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border shrink-0">
<h2 className="text-lg font-semibold text-foreground">Help</h2>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-5 space-y-6">
{/* Getting started */}
<section className="space-y-2">
<div className="flex items-center gap-2 text-foreground">
<BookOpen className="h-4 w-4" />
<h3 className="text-sm font-semibold">Getting Started</h3>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
Select a tool from the sidebar or search for one with{" "}
<Kbd keys="mod+k" />. Upload an image by dragging it onto the
page or clicking the upload area. Adjust settings and download
your result.
</p>
</section>
{/* Keyboard shortcuts */}
<section className="space-y-3">
<div className="flex items-center gap-2 text-foreground">
<Keyboard className="h-4 w-4" />
<h3 className="text-sm font-semibold">Keyboard Shortcuts</h3>
</div>
<div className="rounded-lg border border-border overflow-hidden">
{SHORTCUTS.map((s, i) => (
<div
key={s.keys}
className={`flex items-center justify-between px-3 py-2 text-sm ${
i !== SHORTCUTS.length - 1
? "border-b border-border"
: ""
}`}
>
<span className="text-muted-foreground">
{s.description}
</span>
<Kbd keys={s.keys} />
</div>
))}
</div>
</section>
{/* Links */}
<section className="space-y-2">
<div className="flex items-center gap-2 text-foreground">
<Github className="h-4 w-4" />
<h3 className="text-sm font-semibold">Resources</h3>
</div>
<div className="flex flex-col gap-1.5">
<a
href="https://github.com/siddharthksah/Stirling-Image"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
>
GitHub Repository
<ExternalLink className="h-3 w-3" />
</a>
<a
href="https://github.com/siddharthksah/Stirling-Image/issues"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
>
Report an Issue
<ExternalLink className="h-3 w-3" />
</a>
<a
href="https://siddharthksah.github.io/Stirling-Image/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
>
Documentation
<ExternalLink className="h-3 w-3" />
</a>
<a
href="/api/docs"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
>
API Reference (Swagger)
<ExternalLink className="h-3 w-3" />
</a>
</div>
</section>
{/* Version */}
<div className="text-xs text-muted-foreground pt-2 border-t border-border">
Stirling Image v{APP_VERSION}
</div>
</div>
</div>
</div>
);
}
function Kbd({ keys }: { keys: string }) {
return (
<kbd className="px-1.5 py-0.5 rounded bg-muted border border-border text-xs font-mono text-muted-foreground">
{formatShortcut(keys)}
</kbd>
);
}
@@ -67,7 +67,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
/>
{/* Dialog */}
<div className="relative bg-background border border-border rounded-xl shadow-2xl w-full max-w-3xl max-h-[85vh] flex overflow-hidden">
<div className="relative bg-background border border-border rounded-xl shadow-2xl w-full max-w-3xl h-[85vh] flex overflow-hidden">
{/* Sidebar nav */}
<div className="w-48 border-r border-border bg-muted/30 p-3 space-y-1 shrink-0">
<div className="flex items-center justify-between mb-4 px-2">
@@ -216,8 +216,8 @@ function SystemSection() {
const [saveMsg, setSaveMsg] = useState<string | null>(null);
useEffect(() => {
apiGet<Record<string, string>>("/v1/settings")
.then((data) => setSettings(data))
apiGet<{ settings: Record<string, string> }>("/v1/settings")
.then((data) => setSettings(data.settings))
.catch(() => {
// Fallback defaults if endpoint not ready
setSettings({
@@ -645,8 +645,8 @@ function ApiKeysSection() {
const loadKeys = useCallback(async () => {
try {
const data = await apiGet<{ keys: ApiKeyEntry[] }>("/v1/api-keys");
setKeys(data.keys);
const data = await apiGet<{ apiKeys: ApiKeyEntry[] }>("/v1/api-keys");
setKeys(data.apiKeys);
} catch {
setKeys([]);
} finally {
@@ -823,13 +823,21 @@ function AboutSection() {
>
GitHub Repository
</a>
<a
href="https://siddharthksah.github.io/Stirling-Image/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
>
Documentation
</a>
<a
href="/api/docs"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
>
API Documentation
API Reference (Swagger)
</a>
</div>
</div>