Web panel for GPT Image 2 API image generation and editing.
English | 中文
GPT Image Panel is a lightweight FastAPI web UI for image generation and image editing. It is designed as a self-hosted panel that connects to an external GPT-compatible image API and stores generated images locally.
This project is only a self-hosted web visualization panel for initiating image generation and image editing requests. It does not provide, wrap, proxy, resell, or modify any upstream model or API service. All generation capability, content policy enforcement, account permissions, billing, and model behavior come from the upstream API provider configured by the user.
This project does not encourage or endorse generating pornographic, political, illegal, infringing, violent, hateful, or otherwise sensitive or policy-violating content. Users are solely responsible for their configured upstream service, prompts, uploaded images, generated results, storage, sharing, and any legal or platform-policy consequences. Any content generated through an upstream API is unrelated to this project and is not produced, reviewed, hosted, or approved by the project maintainers.
Key characteristics:
- SvelteKit + TypeScript frontend in
frontend/ - FastAPI backend in
backend/app/; usebackend.app.main:appas the ASGI entrypoint - public API paths, methods, status codes, SSE event names, cookies, and response shapes are contract-tested and kept stable
- API presets persisted to SQLite at
data/app.sqlite3 - background generation/edit jobs executed with
asyncio.create_task - local image storage under
images/ - gallery metadata stored in SQLite at
data/app.sqlite3 - Docker and Docker Compose deployment support with frontend static build baked into the image
- pytest API contract tests under
backend/tests/
- API preset management: base URL/path/key, per-preset default model and response format, global SOCKS5 upstream proxy, and global Webhook URL
- prompt helper tags, server-side prompt optimization, independent prompt snippets, and gallery prompt/parameter reuse
- generation and image-editing (
/v1/images/edits) with size/quality/format/compression/quantity controls and up to 16 edit reference images - preview + job history with SSE progress, multi-image result previews,
completed_at, elapsed time, per-job stage timings, loading states, detailed terminal statuses, an errors-only history filter, clear-all for persisted history, cancel for queued/running jobs, and reuse/retry from persisted history - shared queue and concurrency limits for generation/edit jobs
- optional global Webhook URL with HTTPS-only validation, SSRF checks, signing, retry, and masked settings responses
- gallery with filters (FTS-backed prompt search, model, preset, size, date range, favorite), URL-synced page/non-prompt-filter/lightbox/job-history state, direct page-number jump, lightbox previous/next navigation and left/right keyboard shortcuts, “Edit this image”, download, custom delete confirmations with 5-second undo for single images, batch actions with partial-success feedback, delete/delete-all, prompt/image-url copy, and on-demand total-size metadata
- prompt snippets drawer for reusable prompt templates, stored separately from gallery images in SQLite
- ZIP export/import (
metadata.jsonplus streamingmetadata.ndjsonfor new archives) with streaming upload, safety validation, low-memory direct export by default, skipped-entry metadata for partial batch downloads, and visible import/export/download progress states - Cloudflare R2 gallery backup sync: configurable in
.envor Web Settings, health-probed from Settings, dry-run preflighted before manual sync, and manually or periodically synced from Gallery without changing local gallery storage as the source of truth - access-key gate, Host/public-origin allowlist, IP allowlist/proxy-header support, GitHub version badge, and CSP nonce injection
- observability hooks for job stage timings, slow
/api/galleryquery logging, queue/failure metrics, and optional JSON/Prometheus metrics endpoints
The backend is a FastAPI application under backend/app/.
Responsibilities are split into a few modules:
backend/app/main.py— thin ASGI entrypointbackend/app/api/contract_app.py— frozen public API surface and current route wiringbackend/app/schemas/— Pydantic request/response DTOsbackend/app/repositories/— SQLite, gallery, image file, settings, and job persistencebackend/app/integrations/— upstream GPT-compatible image API clientbackend/app/core/— settings, access tokens, IP allowlist, proxy headers, and URL validatorsbackend/app/services/— webhook signing, retry, and async delivery
When serving the static frontend, the backend injects a per-response script nonce into frontend/build/index.html and sends a matching Content Security Policy. Asset and index serving are covered by the backend contract tests.
The frontend is a SvelteKit static application in frontend/.
The backend serves only frontend/build; run a frontend build before starting the production backend.
It uses:
- Tailwind CSS
src/lib/api/client.tsfor same-origin fetch calls to existing/api/*endpointssrc/lib/api/events.tsfor SSE wrappers- stores split across access, settings, prompt snippets, gallery, jobs, preview, and UI state
- components for access, header, settings drawer, prompt snippets drawer, prompt helper, job history drawer, preview, gallery, lightbox, and size selection
Frontend build:
npm --prefix frontend install
npm --prefix frontend run buildRuntime persistent storage is minimal:
- generated images are saved in the
images/directory - gallery metadata, image byte sizes, FTS prompt-search index, and API presets are stored in SQLite at
data/app.sqlite3, includingcompleted_at, Beijing completion time, and generation duration - prompt snippets are stored in SQLite at
data/app.sqlite3independently from gallery metadata - optional R2 backups are incremental object uploads only; local
images/files and SQLite gallery rows remain the only gallery source used for serving, thumbnails, ZIP export, and import - generation/edit job status, errors, timing,
completed_at, and result metadata are stored in SQLite atdata/app.sqlite3; successful multi-image jobs persist the fullimagesresult list while keeping the first result inimage_id/image_urlfor compatibility - image unit scheduling, leases, and parent job aggregation are stored in SQLite, so multiple Granian workers can share generation/edit work without Redis
- frontend calls
/api/generate - backend validates config, creates one SQLite-backed parent job plus one image unit per requested output
- SQLite queue/concurrency limits are enforced globally across Granian workers; progress stages stream via SSE
- generation requests with
n > 1stay as one public job, but count asnqueue units and fan out through SQLite image-unit leases;/v1/images/generationsunit payloads always usen=1 - upstream image data is decoded/downloaded, validated, and saved; gallery metadata is updated
- job history is queryable/streamed (
/api/generate/jobs*), clearable (DELETE /api/generate/jobs/history), cancellable (DELETE /api/generate/{job_id}), and can trigger optional signed webhook callbacks
- frontend selects source images (one or more uploads, optionally combined with one gallery image) and calls
/api/editsor/api/edits/from-gallery/{image_id} - backend creates a job and calls upstream
/v1/images/editsusing multipart form data - source images plus supported edit params are forwarded; multiple references are sent upstream as repeated
image[]fields, and unsupported source formats (for example SVG) are rejected - progress stages stream via SSE; returned image data is decoded/downloaded, validated, and saved
- edited results appear in preview/gallery and follow the same queue, history, and cancellation model as generation
- Python 3.11+
- FastAPI
- Granian
- aiohttp
- SQLite
- Pydantic v2
- SvelteKit
- TypeScript
- Tailwind CSS
LICENSE
README.md
Dockerfile
docker-compose.yml
.env.example
VERSION
requirements.txt
package.json
backend/
requirements-dev.txt
app/
main.py
api/
core/
schemas/
repositories/
integrations/
services/
tests/
frontend/
package.json
svelte.config.js
vite.config.ts
src/
routes/
lib/
api/
stores/
components/
utils/
deploy/
nginx.conf
images/
data/
You need one of the following:
- Python 3.11 or newer
- Docker
- Docker Compose
An external GPT-compatible image API is required for actual image generation.
Direct Docker runs expose the FastAPI/Granian process without nginx. The runtime image configures Granian static serving for public SvelteKit immutable assets under /_app/immutable; protected gallery image/thumbnail/download bytes still go through backend authorization and normal FileResponse.
docker build -t gpt-image-panel .
docker run -d --name gpt-image-panel \
-p 127.0.0.1:9090:9090 \
-v $(pwd)/images:/app/images \
-v $(pwd)/data:/app/data \
gpt-image-panelIf Docker Hub times out while resolving python:3.11-slim, use a reachable mirror image:
docker build \
--build-arg PYTHON_BASE_IMAGE=docker.m.daocloud.io/library/python:3.11-slim \
--build-arg NODE_BASE_IMAGE=docker.m.daocloud.io/library/node:24-alpine \
-t gpt-image-panel .Compose runs ghcr.io/z1rconium/gpt-image-linux:v1.0.3 as one web service on http://127.0.0.1:9090. This version does not use standalone worker containers; background generation, gallery export, R2 sync, thumbnails, SSE, and HTTP requests run inside the Granian/FastAPI service. Override GPT_IMAGE_PANEL_IMAGE only when you intentionally want another image tag.
cp .env.example .env
# edit .env if needed
# ACCESS_KEY is required by default unless ALLOW_UNAUTHENTICATED=true
docker-compose up -d --force-recreateSize Granian to the app capacity instead of CPU count alone. For generation throughput, keep GRANIAN_WORKERS <= MAX_ACTIVE_GENERATE_JOBS; 2-4 workers is usually enough. GRANIAN_BACKPRESSURE limits how many slow requests each worker lets into Python before Granian pushes back.
GRANIAN_WORKERS>1 is supported for generation/edit fan-out plus tracked Gallery export/R2 sync jobs: workers claim SQLite-backed work, direct ZIP downloads share SQLite-backed export capacity slots, SSE subscribers use SQLite global leases, startup maintenance runs once per worker group, GC/scheduled sync use background leader leases, and cancellation marks pending/running generation units without forcibly killing another process.
pip install -r backend/requirements-dev.txt
npm --prefix frontend install
npm run backend:devIn another terminal:
npm run frontend:devThen open http://localhost:5173. The Vite dev server proxies /api and /health to FastAPI at 127.0.0.1:9090, so browser requests stay same-origin in development.
It binds to 127.0.0.1 by default; for LAN/external debugging, run npm run frontend:dev -- --host 0.0.0.0.
For a single-process local smoke test, build the frontend first and run FastAPI through Granian:
npm run frontend:build
granian --interface asgi backend.app.main:app --host 0.0.0.0 --port 9090 --reloadThen open http://localhost:9090.
If you want to run without access auth during local dev, set ALLOW_UNAUTHENTICATED=true. Startup logs a warning in that mode because every non-health API route is unauthenticated when ACCESS_KEY is unset.
curl http://localhost:9090/health- open the site
- optionally use the top-left language switch to toggle English / Simplified Chinese
- click the settings gear icon
- choose an existing preset or click New
- enter the API base URL
- choose the API path
- enter the preset default model; the Generate/Edit form's Model field defaults to the active preset's value
- choose the preset default Response Format; the Generate/Edit form's Response format field defaults to the active preset's value
- enter the API key, or an env ref such as
${OPENAI_API_KEY}; literal keys requireALLOW_PLAINTEXT_SECRETS=true - optionally enter a global SOCKS5 proxy or env ref such as
${UPSTREAM_PROXY_URL} - optionally enter a global Webhook URL or env ref such as
${WEBHOOK_URL}for completed generation/edit jobs - optionally configure R2 Backup with endpoint URL, bucket, prefix, and access key env refs such as
${R2_ACCESS_KEY_ID}/${R2_SECRET_ACCESS_KEY}, then click Test R2 - optionally configure Prompt Optimizer with an endpoint URL, model, timeout seconds, and API key/env ref; literal keys require
ALLOW_PLAINTEXT_SECRETS=true - optionally click Edit System Prompt in the Prompt Optimizer settings to edit the optimizer system prompt stored at
DATA_DIR/prompt_optimizer_system_prompt.md - optionally open Overall Config for
.env.examplevariables that are not already exposed in the main Settings form - optionally run Health check for the saved preset
- click Save Preset
- enter a prompt
- click Prompt Helper tags to append common modifiers
- click Optimize to rewrite the prompt through the server-side optimizer
- open Prompts in the header to save or reuse prompt snippets; using a snippet replaces the current prompt
- choose generation options, including API path for per-request upstream routing
- click Generate
- optionally upload one or more edit reference images, pick "Edit this image" in Gallery/Lightbox, or combine both; uploads append to the current edit sources and Clear removes all edit sources
- click Edits to run image-to-image
- use Gallery/Lightbox "Use prompt" or "Use all" to reuse historical prompt text or full parameters
- view preview and gallery; click Gallery Sync to upload local gallery images missing from the configured R2 bucket prefix
The panel supports these upstream paths. The API base URL may either omit or include /v1; for example, both https://api.example.com and https://api.example.com/v1 are accepted.
- sends generation requests to the Images API
- reads image data from
data[]
- sends generation requests to the Responses API
- sends only
promptandmodelin the upstream request body; the UI model default comes from the active preset - reads base64 image data from
output[]items of typeimage_generation_call - size, quality, format, compression, quantity, and response format controls are disabled in the UI for this path
- sends OpenAI Chat Completions-compatible generation requests
- sends only
model,messages, andstream: falsein the upstream request body - supports
grok-imagine-image-liteand other image models that return image URLs or base64 data through chat completion messages - reads image output from JSON chat completions or
data:SSE chunks, including Markdown image links such as - size, quality, format, compression, quantity, and response format controls are disabled in the UI for this path
- used by the Edits button after image upload(s), gallery-image selection, or both
- always calls
/v1/images/editson the configured API base URL - sends multipart/form-data with source image fields plus supported edit parameters; single uploads use
image, while multiple references are forwarded upstream as repeatedimage[] - supports up to 16 edit reference images total; local uploads append to the current source list and can be combined with one gallery source
- uploaded source files must be supported raster image formats; SVG uploads are rejected
- if the upstream returns
404,405, or501, the UI reports that/v1/images/editsis not supported and stops the edit request
POST /api/settings/presets/{preset_id}/healthvalidates the saved preset without sending a generation request- checks include API path allowability, HTTPS URL/hostname validation, upstream host allowlist and SSRF DNS/private-IP validation, API key/env-ref presence, and a low-cost
OPTIONS/HEADupstream probe - upstream probe failures are diagnostic only:
OPTIONS 404/410is reported as a warning because many GPT-compatible gateways only implementPOST, and an isolatedupstream_probeerror does not fail the overall preset health - returned shape is
{ status, checks: [{ name, status, message }] }, where each status isok,warning, orerror - API key env refs use the exact
${ENV_VAR_NAME}form; the database stores the reference string and generation/edit calls resolve it from the server environment at request time - literal API keys are rejected by default; set
ALLOW_PLAINTEXT_SECRETS=trueonly if you intentionally want plaintext SQLite storage
- Settings includes an
Overall Configmodal for.env.examplevariables that are not already managed by the main Settings form. - On startup, the server syncs current process environment values into SQLite. Effective priority is
SQLite override > current env > code default. - Saving in the modal writes SQLite overrides; it does not rewrite
.env. Container rebuilds keep saved overrides as long asdata/app.sqlite3is preserved. - Secret values are stored as real override values when saved, but API responses and the UI only return masked values. Sending
********preserves the existing secret override;Reset to .envclears that override. - Hot-reloadable fields update in memory immediately. Runtime paths, access/session startup behavior, queue concurrency, and Docker build image values are marked as restart-required or build-only.
DATABASE_FILE,DATA_DIR,IMAGES_DIR, andTHUMBNAILS_DIRcan be displayed and saved as overrides, but the running process cannot move the current SQLite connection or storage directories without restart.
- The Settings drawer has one global
SOCKS5 proxyfield, independent of API presets. - Leave it empty for direct upstream API calls.
- Use an env ref such as
${UPSTREAM_PROXY_URL}by default. Literal values likesocks5://host:portorsocks5://user:pass@host:portrequireALLOW_PLAINTEXT_SECRETS=true; stored proxy passwords are masked in API responses and the UI. - The proxy boundary is intentionally narrow: only generation/edit upstream API
POSTcalls use it. Preset health checks, webhooks, version checks, frontend/api/*requests, and image URL downloads stay direct.
- The Settings drawer has one global
Webhook URLfield directly belowSOCKS5 proxy; it is independent of API presets. - Leave it empty to disable job callbacks.
- Use an env ref such as
${WEBHOOK_URL}by default. Literal webhook URLs requireALLOW_PLAINTEXT_SECRETS=true. - When configured, completed generation/edit jobs send signed webhook callbacks to that HTTPS URL.
WEBHOOK_SIGNING_SECRETis required when a Webhook URL is configured. Stored webhook URLs are masked in API responses and the UI.
- R2 Backup is an incremental backup path, not remote gallery storage.
- Configure it in
.envwithR2_BACKUP_ENABLED=trueplus the otherR2_*variables, or in Web Settings. When SQLite has no saved R2 settings yet, startup persists the current.envR2 values so they appear in Web Settings. Later Web Settings saves take precedence. Web Settings accepts${ENV_VAR_NAME}refs for credentials; literal stored credentials requireALLOW_PLAINTEXT_SECRETS=true. - Test R2 in Settings runs
HeadBucket, a prefix-scopedListObjectsV2, and a small probe object write; probe cleanup failure is reported as a warning. - The Gallery Sync button first runs a dry-run preflight showing compared, pending upload, skipped-existing, and missing-local counts. Confirmed uploads are recorded in SQLite and normal sync only compares local filenames that are new or changed for the current
R2_KEY_PREFIX; existing bucket objects are skipped, and bucket-only objects are never deleted or overwritten. Full reconcile mode rechecks local filenames against R2 for cases such as remote manual deletion and stores a filename checkpoint so an expired job can resume after the last completed page. R2_SYNC_CONCURRENCYcontrols bounded concurrent R2HEAD/upload workers; the default is4, which is usually better for many small images than fully serial sync.- Set
R2_SYNC_INTERVAL_HOURSor the Web Settings interval to a positive integer to run the same incremental sync periodically.0disables automatic sync, and startup waits one full interval before the first scheduled run.
auto— default; let the model choose the output size- ratio presets — 1K, 2K, or 4K with ratios
1:1,4:3,3:4,16:9,9:16, or21:9 - custom width and height — values are normalized to multiples of 16, max side
3840px, aspect ratio up to3:1, and total pixels between655360and8294400
- Quality:
auto,low,medium, orhigh - Format:
PNG,JPEG, orWebP - Compression: disabled for
PNG;0-100forJPEGandWebP - Quantity: integer from
1to10; the field can be cleared while editing, and Generate/Edit will restore an empty value to1on submit - Response Format: defaults to the active preset's value (
urlby default), withnoneandb64_jsonstill available;noneomits theresponse_formatparameter
- each uploaded source image is limited by
MAX_FILE_SIZE_MB, must pass full Pillow decode validation plus pixel-bomb limits, and must be a supported raster format (.png,.jpg,.jpeg,.webp,.gif,.avif,.bmp,.heic,.heif,.ico,.tif,.tiff); SVG is rejected /api/importaccepts ZIP archives created by/api/download-all- import archives must include
metadata.jsonormetadata.ndjson; new exports include both, and import prefersmetadata.ndjsonto avoid loading large metadata arrays into memory while keepingmetadata.jsonfor older versions - selected-image ZIP downloads include
metadata.skippedwhen requested gallery rows or image files are missing - import archives are validated for uploaded size, file count, total uncompressed size, metadata size, member-path safety, and compression ratio
- imported image entries must pass file extension, content-type/magic-byte, full decoder, and pixel-count validation before they are stored
All variables listed in .env.example are also tracked in SQLite for Overall Config. Variables already exposed in the main Settings form stay there; the Overall Config modal shows the remaining runtime/security/limit/build knobs.
| Variable | Default | Description |
|---|---|---|
DEFAULT_API_URL |
empty | Pre-fill API base URL; may omit or include /v1 |
DEFAULT_API_KEY |
empty | Pre-fill API key; use an env ref such as ${OPENAI_API_KEY} by default. Literal keys require ALLOW_PLAINTEXT_SECRETS=true |
DEFAULT_API_PATH |
/v1/images/generations |
Default upstream path; supported values are /v1/images/generations, /v1/responses, and /v1/chat/completions |
DEFAULT_RESPONSES_MODEL |
gpt-5.4 |
Fallback top-level model used for /v1/responses when no request/preset model is provided |
DEFAULT_UPSTREAM_SOCKS5_PROXY |
empty | Optional default global SOCKS5 proxy or env ref for generation/edit upstream API calls |
AIOHTTP_CONNECTION_LIMIT |
100 |
Total aiohttp connector connection limit for upstream/probe/download calls |
AIOHTTP_CONNECTION_LIMIT_PER_HOST |
20 |
Per-host aiohttp connector connection limit; 0 disables the per-host cap |
ALLOW_PLAINTEXT_SECRETS |
false |
Allow literal API keys / SOCKS5 proxy URLs / Webhook URLs / R2 credentials to be persisted in SQLite instead of requiring ${ENV_VAR_NAME} refs |
PROMPT_OPTIMIZER_ENABLED |
false |
Enable the server-side prompt optimizer |
PROMPT_OPTIMIZER_API_URL |
empty | Full Chat Completions-compatible endpoint URL used by the prompt optimizer |
PROMPT_OPTIMIZER_API_KEY |
empty | Prompt optimizer API key; use an env ref such as ${OPENAI_API_KEY} by default. Literal keys require ALLOW_PLAINTEXT_SECRETS=true |
PROMPT_OPTIMIZER_MODEL |
gpt-4o-mini |
Prompt optimizer model |
PROMPT_OPTIMIZER_TIMEOUT_SECONDS |
60 |
Prompt optimizer request timeout in seconds |
PROMPT_OPTIMIZER_MAX_OUTPUT_CHARS |
4000 |
Max optimized prompt length returned to the textarea |
PROMPT_OPTIMIZER_MAX_RESPONSE_MB |
8 |
Max optimizer upstream response body size in MB before JSON parsing |
PROMPT_OPTIMIZER_HOST_ALLOWLIST |
empty | Optional comma-separated hostname allowlist for the optimizer endpoint |
R2_BACKUP_ENABLED |
false |
Enable Gallery Sync backup when using environment-based R2 configuration |
R2_ENDPOINT_URL |
empty | Cloudflare R2 S3-compatible endpoint URL, for example https://ACCOUNT_ID.r2.cloudflarestorage.com |
R2_BUCKET_NAME |
empty | Bucket used by Gallery Sync backup |
R2_REGION |
auto |
S3 client region name for R2 |
R2_KEY_PREFIX |
gallery/ |
Object key prefix for gallery backups; use a dedicated prefix such as gallery-test/ for validation |
R2_SYNC_INTERVAL_HOURS |
0 |
Scheduled Gallery Sync interval in hours; 0 disables automatic sync |
R2_SYNC_CONCURRENCY |
4 |
Concurrent R2 HEAD/upload workers used by Gallery Sync |
R2_ACCESS_KEY_ID |
empty | R2 access key ID used by env-ref credentials |
R2_SECRET_ACCESS_KEY |
empty | R2 secret access key used by env-ref credentials |
APP_VERSION |
VERSION file |
Override the app version shown in the UI and returned by /api/version; read on each request |
GITHUB_REPO |
Z1rconium/gpt-image-linux |
GitHub owner/repo used for latest-release update detection; set empty to disable latest-version checks |
ENABLE_VERSION_CHECK |
true |
Enable per-request GitHub latest-version checks for the header New badge |
VERSION_CHECK_TIMEOUT_SECONDS |
3 |
Timeout for each GitHub latest release or branch VERSION request |
VERSION_CHECK_BRANCH |
main |
Branch used for the fallback raw VERSION file check |
ENABLE_METRICS |
false |
Enable /api/metrics JSON counters/gauges/rates/latency summaries and Prometheus text output |
SLOW_GALLERY_QUERY_MS |
200 |
Log /api/gallery requests at or above this threshold with prompt presence/length/hash, other filters, page, total, DB query time, and rows/count/total-bytes/filter-options timings |
ENABLE_NGINX_ACCEL_REDIRECT |
false |
Return X-Accel-Redirect for authorized image/thumb/download responses when nginx internal aliases are in front; Compose enables this by default |
PUBLIC_IMAGE_BASE_URL |
empty | Optional public/CDN base URL for gallery image bytes; when set, Gallery and job image URLs point there while /api/image remains as an authorized fallback |
PUBLIC_THUMBNAIL_BASE_URL |
empty | Optional public/CDN base URL for generated thumbnail bytes; when set, Gallery thumbnail URLs point there while /api/thumb remains as an authorized fallback |
ACCESS_KEY |
empty | Required by default; all non-health routes require unlock when set |
ALLOW_UNAUTHENTICATED |
false |
Set true to explicitly allow startup without ACCESS_KEY; startup logs a warning because non-health API routes are unauthenticated |
ACCESS_KEY_COOKIE_NAME |
gpt_image_access |
Browser cookie name used for the access-key session |
ACCESS_COOKIE_SECURE |
true |
Mark the access cookie Secure; set false only for plain-HTTP local/private deployments |
ACCESS_MAX_FAILURES |
5 |
Failed access-key attempts before temporary lockout |
ACCESS_LOCKOUT_SECONDS |
300 |
Lockout duration after too many failed access attempts |
IP_ALLOWLIST |
empty | Comma-separated allowed IPs/CIDRs |
TRUST_PROXY_HEADERS |
false |
Read X-Forwarded-For, X-Real-IP, X-Forwarded-Proto, or X-Forwarded-Host from a trusted reverse proxy |
TRUSTED_PROXY_IPS |
empty | Optional comma-separated proxy IPs/CIDRs allowed to provide trusted proxy headers |
PUBLIC_ORIGIN |
empty | Optional canonical browser origin such as https://panel.example.com; also contributes to the Host allowlist and CSRF expected origin |
ALLOWED_HOSTS |
empty | Optional comma-separated allowed Host / trusted X-Forwarded-Host values; accepts hostnames, host:port, or origins |
CSRF_ORIGIN_CHECK_ENABLED |
true |
Reject unsafe POST, PATCH, and DELETE requests unless Origin, Referer, or same-origin Sec-Fetch-Site proves the browser source |
UPSTREAM_HOST_ALLOWLIST |
empty | Optional comma-separated hostname allowlist for generation/edit upstream API URLs; with a SOCKS5 upstream proxy, the proxy is the trust boundary for remote DNS/network reachability |
WEBHOOK_HOST_ALLOWLIST |
empty | Optional comma-separated webhook hostname allowlist |
MAX_FILE_SIZE_MB |
50 |
Max uploaded image size in MB for edit source images, imported image files, and downloaded upstream image URLs |
MAX_JSON_BODY_MB |
1 |
Max JSON request body size in MB for non-upload API calls |
MAX_UPSTREAM_JSON_MB |
128 |
Max upstream JSON/SSE response body size in MB before parsing; prefer response_format=url for large or multi-image results |
MAX_IMAGE_PIXELS |
100000000 |
Max decoded image pixels accepted by Pillow before decompression-bomb rejection |
IMPORT_ARCHIVE_MAX_MB |
1000 |
Max uploaded ZIP size in MB for /api/import |
IMPORT_MAX_FILES |
500 |
Max number of files allowed inside one import archive |
IMPORT_MAX_UNCOMPRESSED_MB |
1024 |
Max total uncompressed size in MB across all files in an import archive |
IMPORT_MAX_METADATA_BYTES |
2097152 |
Max metadata.json size in bytes for an import archive |
IMPORT_MAX_COMPRESSION_RATIO |
100 |
Max allowed uncompressed/compressed ratio for any imported file |
GRANIAN_WORKERS |
1 |
Granian worker processes; keep <= MAX_ACTIVE_GENERATE_JOBS for generation throughput |
GRANIAN_RUNTIME_THREADS |
2 |
Granian Rust runtime threads per worker; try 1 first when using multiple workers behind nginx/HTTP/1 |
GRANIAN_LOOP |
uvloop |
Granian event loop implementation |
GRANIAN_BACKPRESSURE |
100 |
Per-worker request backpressure before slow requests stop entering Python |
GRANIAN_BACKLOG |
2048 |
Socket listen backlog for pending connections |
GRANIAN_STATIC_PATH_ROUTE |
/_app/immutable |
Direct-Docker public immutable asset route served by Granian |
GRANIAN_STATIC_PATH_MOUNT |
/app/frontend/build/_app/immutable |
Direct-Docker immutable asset directory mounted into Granian static serving |
GRANIAN_STATIC_PATH_EXPIRES |
31536000 |
Granian static asset max-age seconds |
MAX_ACTIVE_GENERATE_JOBS |
2 |
Max number of generation/edit image units running globally |
MAX_QUEUED_GENERATE_JOBS |
20 |
Max additional queued generation/edit image units before new requests are rejected with 429 |
IMAGE_JOB_UNIT_LEASE_SECONDS |
120 |
SQLite claim lease for a running image unit before another worker may retry it |
IMAGE_JOB_UNIT_POLL_INTERVAL_SECONDS |
0.35 |
Minimum SQLite polling interval for image-unit dispatch and generation SSE; dispatchers back off when the queue is empty |
MAX_PENDING_EDIT_SOURCE_MB |
200 |
Global SQLite-reserved pending edit source image bytes in MB; set 0 to disable this byte cap |
MAX_SSE_SUBSCRIBERS_GLOBAL |
200 |
Max active SSE subscribers across all Granian workers via SQLite slot leases |
MAX_SSE_SUBSCRIBERS_PER_IP |
10 |
Max active SSE subscribers per client IP |
SSE_CONNECTION_TTL_SECONDS |
3600 |
Maximum lifetime for one SSE connection |
IMAGES_DIR |
./images |
Directory for saved images |
THUMBNAILS_DIR |
./images/thumbs |
Directory for generated gallery thumbnails |
THUMBNAIL_MAX_SIDE |
512 |
Max thumbnail width/height in pixels |
THUMBNAIL_CPU_CONCURRENCY |
1 |
Global SQLite-limited thumbnail generation concurrency across workers |
DATA_DIR |
./data |
Directory for SQLite runtime data |
DATABASE_FILE |
./data/app.sqlite3 |
SQLite database for gallery metadata and API presets |
PYTHON_BASE_IMAGE |
python:3.11-slim |
Docker build base image; override when Docker Hub is slow or blocked |
NODE_BASE_IMAGE |
node:24-alpine |
Docker frontend build base image; override when Docker Hub is slow or blocked |
WEBHOOK_SIGNING_SECRET |
empty | Required when a global Webhook URL is configured; used to sign webhook payloads (X-Webhook-Signature) |
WEBHOOK_TIMEOUT_SECONDS |
5 |
Webhook delivery timeout per attempt (seconds) |
WEBHOOK_MAX_ATTEMPTS |
3 |
Max webhook delivery retry attempts |
| Method | Path | Description |
|---|---|---|
GET |
/ |
Frontend UI |
GET |
/health |
Health check |
GET |
/api/version |
Current app version, configured GitHub repo, and latest-release URL; requires access unlock when ACCESS_KEY is set |
GET |
/api/access/status |
Check access-key session status |
POST |
/api/access |
Unlock access for 3 hours |
POST |
/api/settings |
Save the active API preset |
GET |
/api/settings |
Get current settings and presets |
GET |
/api/settings/overall-config |
List environment-backed Overall Config fields, masked secrets, sources, and restart/build-only metadata |
PUT |
/api/settings/overall-config |
Save or clear SQLite overrides for Overall Config fields |
POST |
/api/settings/r2/health |
Validate a draft R2 Backup config and run bucket/list/write checks |
POST |
/api/prompt/optimize |
Rewrite a prompt through the server-side optimizer |
GET |
/api/prompt/optimizer-system-prompt |
Read the Prompt Optimizer system prompt, falling back to the built-in default |
POST |
/api/prompt/optimizer-system-prompt |
Save the Prompt Optimizer system prompt to DATA_DIR/prompt_optimizer_system_prompt.md |
GET |
/api/prompt-snippets |
List prompt snippets, optionally filtered by query |
POST |
/api/prompt-snippets |
Create a prompt snippet |
PATCH |
/api/prompt-snippets/{snippet_id} |
Update a prompt snippet title, prompt, or favorite flag |
DELETE |
/api/prompt-snippets/{snippet_id} |
Delete a prompt snippet |
POST |
/api/settings/presets |
Create and activate an API preset |
POST |
/api/settings/presets/{preset_id}/activate |
Activate an API preset |
POST |
/api/settings/presets/{preset_id}/health |
Validate a saved API preset and run a low-cost upstream probe |
DELETE |
/api/settings/presets/{preset_id} |
Delete an API preset |
POST |
/api/generate |
Start an image generation job |
POST |
/api/edits |
Start an image edit job with one or more multipart image uploads |
POST |
/api/edits/from-gallery/{image_id} |
Start an image edit job using an existing gallery image, optionally with uploaded references |
GET |
/api/generate/jobs |
List queued/running generation and edit jobs; pass include_finished=true with optional limit/offset for paginated persisted history, and failed_only=true to return only error/upstream_error history rows |
GET |
/api/generate/jobs/events |
Stream queued/running generation and edit jobs over SSE |
DELETE |
/api/generate/jobs/history |
Delete all persisted terminal generation/edit job history rows; queued/running jobs and gallery images are kept |
GET |
/api/generate/{job_id} |
Get generation job status or result |
GET |
/api/generate/{job_id}/events |
Stream generation job status/progress over SSE |
DELETE |
/api/generate/{job_id} |
Cancel and remove a queued/running generation or edit job |
GET |
/api/gallery |
List gallery images with page or cursor pagination and optional prompt, model, preset, size, date_from, date_to, favorite, include_total_bytes, include_counts, and include_filter_options filters; image rows include thumbnail_status (ready, queued, or missing) |
PATCH |
/api/gallery/{id}/favorite |
Set or clear a gallery favorite flag |
GET |
/api/gallery/{image_id} |
Get a single gallery entry by ID |
GET |
/api/image/{filename} |
Serve image file |
GET |
/api/thumb/{filename} |
Serve an existing WebP gallery thumbnail; missing thumbnails are queued for the background worker |
GET |
/api/download/{filename} |
Download image as attachment |
DELETE |
/api/gallery/{id} |
Delete gallery entry and its server image file |
GET |
/api/download-all |
Download all gallery images plus metadata.json/metadata.ndjson as a low-memory streaming ZIP file; accepts export_job_id from a direct export job |
POST |
/api/gallery/direct-export-jobs |
Reserve a direct streaming ZIP export job for the full gallery and return its download_url |
GET |
/api/gallery/direct-export-jobs/{job_id} |
Get direct streaming ZIP export job status |
GET |
/api/gallery/direct-export-jobs/{job_id}/events |
Stream direct ZIP prepare/output progress over SSE |
POST |
/api/gallery/export-jobs |
Start a tracked gallery ZIP export job; optionally pass ids for selected images |
GET |
/api/gallery/export-jobs/{job_id} |
Get tracked ZIP export job status |
GET |
/api/gallery/export-jobs/{job_id}/events |
Stream ZIP export pack progress over SSE |
GET |
/api/gallery/export-jobs/{job_id}/download |
Download a completed tracked ZIP export with Content-Length transfer progress |
POST |
/api/gallery/sync-jobs |
Start a single active R2 gallery backup sync job; accepts dry_run and full_reconcile |
GET |
/api/gallery/sync-jobs/{job_id} |
Get R2 sync job status |
GET |
/api/gallery/sync-jobs/{job_id}/events |
Stream R2 sync progress over SSE |
POST |
/api/import |
Import a ZIP created by /api/download-all; pass async_job=true to create a progress-tracked import job |
GET |
/api/gallery/import-jobs/{job_id} |
Get progress-tracked Gallery import job status |
GET |
/api/gallery/import-jobs/{job_id}/events |
Stream Gallery import progress over SSE |
DELETE |
/api/gallery |
Delete all gallery entries and server image files |
GET |
/api/metrics |
Optional metrics snapshot; returns JSON by default or Prometheus exposition format with Accept: text/plain; only available when ENABLE_METRICS=true |
GET |
/api/metrics/prometheus |
Optional Prometheus exposition metrics; only available when ENABLE_METRICS=true |
- app version comes from
APP_VERSIONthenVERSION; both the local app version and optional GitHub remote check are evaluated on each web-triggered version request after access unlock. The remote check reads the latest release first, falls back to the configured branchVERSION, and can show aNewbadge without blocking usage. - when
PUBLIC_ORIGINorALLOWED_HOSTSis configured, unknownHostor trustedX-Forwarded-Hostvalues are rejected before CSRF checks; unsafe browser requests must includeOrigin,Referer, or same-originSec-Fetch-Site, andSec-Fetch-Site: same-origindoes not bypass the Host allowlist - upstream-returned image URLs must use HTTPS; plain HTTP image downloads are rejected before the backend fetches them
- when an upstream SOCKS5 proxy is configured, the backend still validates the upstream URL before connecting and logs a warning if the hostname resolves locally to private/internal IPs, but the SOCKS5 proxy is the trust boundary for remote DNS and network reachability
- presets, prompt snippets, and gallery/job data persist only in
DATABASE_FILE - SQLite repository operations use short-lived connections with WAL enabled at startup;
DATA_DIRis chmodded to0700, the SQLite DB/sidecars are chmodded to0600, and app shutdown/tests call the storage close hook so connection lifecycle stays explicit - generation and edit share one SQLite queue measured in image units (
MAX_ACTIVE_GENERATE_JOBS+MAX_QUEUED_GENERATE_JOBS); enqueue capacity, parent job creation, unit insertion, and edit-source byte reservation are committed atomically in SQLite. Edit source images are staged underDATA_DIR/edit-sources, globally capped byMAX_PENDING_EDIT_SOURCE_MB, released on terminal/cancelled jobs, support cancellation, and persist terminal history includingcompleted_at - batch generation/edit (
n > 1) consumesnqueue units; the parent job aggregates successful unit results intoimages[], Gallery metadata keeps the user-requestedn, and units can be claimed by different Granian workers - tracked Gallery export jobs and manual/scheduled R2 sync jobs persist in SQLite, so create/query/SSE/download work across
GRANIAN_WORKERS>1; workers claim queued or expired running jobs with SQLite leases, empty dispatchers back off, GC/scheduled sync use background leader leases, export ZIP files live under sharedDATA_DIR/exports, and direct ZIP downloads reserve the same global export capacity through short-lived SQLite slot rows - SSE subscriber limits are enforced through SQLite
sse_slots, soMAX_SSE_SUBSCRIBERS_GLOBALis no longer multiplied by Granian worker count; stale slots expire if a worker dies before release - Gallery thumbnails are queued on image save/import and on missing-thumbnail access:
/api/thumb/{filename}only serves an existing file or queues a missing one, Gallery rows exposethumbnail_status, and the background worker runs Pillow under the globalTHUMBNAIL_CPU_CONCURRENCYslot limit - Prompt Optimizer uses its own server-side Chat Completions-compatible endpoint config and user-configurable request timeout/response-size cap, resolves API key env refs on the backend, stores its editable system prompt in
DATA_DIR/prompt_optimizer_system_prompt.md, and does not consume generation/edit queue capacity. - R2 Backup uses boto3 against a Cloudflare R2 S3-compatible endpoint under
asyncio.to_thread; sync jobs list only the configured prefix, retain only keys that match local gallery filenames, fall back to per-fileHeadObjectchecks for very large prefixes, support dry-run preflight counts, store per-file remote-seen metadata, upload missing local gallery filenames, and never serve, overwrite, or delete gallery images from R2. - SSE is the primary progress channel;
/api/generate/jobsprovides list/history (include_finished=true, optionallimit/offset, optionalfailed_only=true),/api/generate/jobs/historyclears terminal history, and/api/generate/jobs/eventsstreams SQLite-backed live job-list changes with sub-second polling - terminal job history includes
stage_timingsforupstream_wait,download_decode,validate, anddb_insert; slow gallery queries are logged with prompt presence/length/hash plus other filters, totals, cursor/count flags, and rows/count/total-bytes/filter-options timings and counted in metrics; optional metrics includeworker_id, recent worker snapshots, queue depth, running jobs, failure ratios, job-stage latencies, SQLite busy/slow counters, SSE poll query counters, claim misses, active SSE slots, and worker heartbeat age; terminal job statuses distinguishcancelled,interrupted, andupstream_errorin addition to the genericerror - upstream JSON/SSE bodies are read with a
MAX_UPSTREAM_JSON_MBcap before parsing, JSON request bodies are capped byMAX_JSON_BODY_MB, and upstream image URL downloads are revalidated (SSRF-aware, no blind redirect follow), fully decoded with Pillow, pixel-limited byMAX_IMAGE_PIXELS, and bounded byMAX_FILE_SIZE_MB /api/importenforces ZIP safety/size/count/compression checks and can run as a progress-tracked import job withasync_job=true;/api/download-allkeeps the low-memory streaming path with SQLite-backed direct export jobs/slots and reports server-side prepare/output progress over SSE when called withexport_job_id. Full Gallery export uses direct streaming by default. Tracked export jobs still write temp ZIP files for selected/small download flows withContent-Lengthtransfer progress. ZIP metadata is written as streamedmetadata.jsonplusmetadata.ndjson, uses storedsha256/byteswhen available, and does not re-read image files only to backfill missing hashes during export.- gallery stores byte-size metadata and thumbnails (
THUMBNAILS_DIR), with older thumbnails queued for background backfill on access, opt-in byte-size backfill for older images, and a background file GC that removes unreferenced orphan images/thumbnails after a short TTL while preserving SQLite-referenced filenames; with Compose, nginx serves immutable frontend assets directly and serves image/thumb/download file bytes only after FastAPI returns an authorizedX-Accel-Redirect; withPUBLIC_IMAGE_BASE_URL/PUBLIC_THUMBNAIL_BASE_URL, Gallery and job responses can point directly at object storage/CDN URLs - startup reconciliation removes gallery rows for missing files and marks previously running/queued jobs as interrupted
npm run frontend:check
npm run frontend:build
python3 -m pytest backend/tests/test_contract.py -q
npm run test:e2e
RUN_PERFORMANCE_TESTS=true python3 -m pytest backend/tests/test_performance.py -q
npm run test:e2e:perfThe contract tests cover the frozen public API surface, including access cookies, settings, generation/edit job creation, job timing metrics, SSE response framing, gallery import/export, frontend index/CSP handling, static asset access, downloads, validation errors, and 500 error shape. Playwright covers access gate, settings drawer focus behavior, mocked generate/edit flows, gallery filtering/page jumps/batch actions, toast live regions, and lightbox keyboard close/navigation.
Contributions are welcome.
Helpful guidelines:
- keep backend changes simple and explicit
- update
VERSIONwhen a user-visible change or release-worthy fix warrants a newvMAJOR.MINOR.PATCHversion - use FastAPI response models from
backend/app/schemas/where applicable - keep persistent storage operations centralized in
backend/app/repositories/ - keep upstream API interaction centralized in
backend/app/integrations/ - keep browser requests on same-origin
/api/*paths; do not introduce direct cross-origin frontend-to-backend calls - avoid storing real API keys in repository files
- do not commit generated images or runtime gallery metadata unless explicitly requested
- preserve the existing async generation flow and SSE progress model unless the change explicitly requires altering job lifecycle behavior
This project is licensed under CC BY-NC 4.0 (Creative Commons Attribution-NonCommercial 4.0 International).
- You can use, copy, modify, redistribute, and create derivative works.
- You must provide attribution and keep the license notice.
- You may not use this project or derivative works for commercial purposes.
- If you need commercial use, you must obtain prior permission from the copyright holder.
See LICENSE for the repository license text.
GPT Image 2 API 图像生成和编辑 Web 面板。
English | 中文
GPT Image Panel 是一个轻量级 FastAPI Web 界面,用于图像生成和图像编辑。它被设计为自托管面板,连接外部 GPT 兼容图像 API,并在本地存储生成的图片。
本项目仅是一个用于发起图像生成和图像编辑请求的自托管 Web 可视化面板。本项目不提供、不封装、不代理、不转售、也不修改任何上游模型或 API 服务。实际生成能力、内容政策执行、账号权限、计费规则和模型行为均来自用户自行配置的上游 API 服务商。
本项目不鼓励、不支持生成色情、政治、违法、侵权、暴力、仇恨,或其他敏感及违反政策的内容。用户需要自行对其配置的上游服务、提示词、上传图片、生成结果、存储、传播,以及由此产生的法律或平台政策后果负责。任何通过上游 API 生成的内容均与本项目无关,亦不代表项目维护者生成、审核、托管或认可该内容。
主要特点:
- SvelteKit + TypeScript 前端位于
frontend/ - FastAPI 后端位于
backend/app/;ASGI 入口使用backend.app.main:app - 公共 API 路径、方法、状态码、SSE 事件名、cookie 和响应结构通过契约测试冻结
- API 预设持久化保存在 SQLite:
data/app.sqlite3 - 生成/编辑任务通过
asyncio.create_task异步执行 - 图片保存在
images/ - Gallery 元数据保存在 SQLite:
data/app.sqlite3 - Docker 镜像会构建并内置 SvelteKit 静态前端
- pytest API 契约测试位于
backend/tests/
- API 预设管理:base URL/path/key、每个预设的默认 model 和 Response Format、全局 SOCKS5 上游代理和全局 Webhook URL
- 提示词助手标签、服务端提示词优化器、独立提示词收藏夹,以及 Gallery 提示词/参数复用
- 图像生成 + 图生图编辑(
/v1/images/edits),支持尺寸/质量/格式/压缩率/数量等参数,并支持最多 16 张编辑参考图 - 预览 + 历史任务:SSE 进度、多图结果预览、
completed_at、耗时、任务分段耗时、加载状态、细分终态状态、仅错误历史筛选、清空持久化历史、排队/运行任务取消,以及从持久化历史复用/重试 - 生成与编辑共享并发和排队限制
- 可选全局 Webhook URL:HTTPS 校验、SSRF 防护、签名、重试,以及设置响应打码
- Gallery:筛选(FTS 提示词搜索、模型、预设、尺寸、日期区间、收藏)、URL 同步的 page/非提示词筛选/lightbox/job history 状态、页码输入跳转、Lightbox 上一张/下一张导航和左右方向键快捷键、”Edit this image”、下载/删除、批量操作部分成功反馈、单图 5 秒撤销删除、复制提示词/图片链接、按需总大小统计
- 提示词收藏夹:可复用 prompt 模板,与 Gallery 图片分开存储和管理
- ZIP 导出导入(含
metadata.json)+ 流式上传 + 安全校验 + 低内存导出路径 + 批量下载 skipped metadata + 可见导入/导出/下载进度状态 - Cloudflare R2 Gallery 备份同步:可通过
.env或 Web Settings 配置,可在 Settings 测试连通性,并可从 Gallery 手动或定时增量同步;本地 Gallery 存储仍是唯一源数据 - 访问密钥、Host/public origin 白名单、IP 白名单/反向代理头、版本检测、CSP nonce
- 观测能力:任务分段耗时、慢
/api/gallery查询日志、队列/失败率指标、可选 JSON/Prometheus metrics
后端是 FastAPI 应用,位于 backend/app/。
功能拆分为以下模块:
backend/app/main.py— 很薄的 ASGI 入口backend/app/api/contract_app.py— 冻结的公共 API 表面和当前路由组装backend/app/schemas/— Pydantic 请求/响应 DTObackend/app/repositories/— SQLite、Gallery、图片文件、settings 和 jobs 持久化backend/app/integrations/— 上游 GPT 兼容图片 API 调用backend/app/core/— settings、访问 token、IP allowlist、proxy header 和 URL 校验backend/app/services/— webhook 签名、重试和异步投递
后端服务静态前端时,会为 frontend/build/index.html 注入每次响应不同的 script nonce,并发送匹配的 Content Security Policy。前端入口和静态资源访问已纳入后端契约测试。
前端是 SvelteKit 静态应用,位于 frontend/。
后端只服务 frontend/build;生产方式启动后端前需要先完成前端构建。
使用技术:
- Tailwind CSS
src/lib/api/client.ts封装同源/api/*fetchsrc/lib/api/events.ts封装 SSE- stores 拆分为 access、settings、prompt snippets、gallery、gallery actions、edit source、jobs、preview、lightbox、version 和 UI
- 组件拆分为 access、header、settings drawer、prompt snippets drawer、prompt helper、job history drawer、preview、gallery、lightbox 和 size dialog
前端构建命令:
npm --prefix frontend install
npm --prefix frontend run build运行时持久化存储非常简单:
- 生成的图片保存在
images/目录 - Gallery 元数据、图片字节数、FTS 提示词索引和 API 预设保存在 SQLite:
data/app.sqlite3,包含真实图片宽高、completed_at完成时间、北京时间生成完成时间和生成耗时 - 提示词收藏夹保存在 SQLite:
data/app.sqlite3,独立于 Gallery 元数据 - 可选 R2 备份只做增量对象上传;本地
images/文件和 SQLite Gallery 行仍是图片服务、缩略图、ZIP 导出和导入流程使用的唯一 Gallery 源 - 生成/编辑任务的状态、错误、耗时、
completed_at、请求参数和结果元数据保存在 SQLite:data/app.sqlite3;多图任务会保留完整images结果列表,同时继续用第一张结果填充image_id/image_url以兼容旧客户端 - image unit 调度、lease 和父任务聚合保存在 SQLite,多 Granian worker 可以不引入 Redis 共享生成/编辑任务
- 前端调用
/api/generate - 后端校验配置,创建一个 SQLite 父任务,并按请求输出数量创建 image unit 子任务
- SQLite 队列/并发限制会跨 Granian worker 全局生效,执行中通过 SSE 推送细分进度
n > 1的生成请求仍只暴露一个父任务,但会按n计入队列容量,并通过 SQLite image-unit lease 分片执行;/v1/images/generationsunit payload 固定使用n=1- 上游返回数据解码/下载、校验并落盘,同时更新 Gallery 元数据
- 任务历史可通过
/api/generate/jobs*查询/订阅,可清空(DELETE /api/generate/jobs/history)或取消运行中任务;可选触发签名 webhook 回调
- 前端选择编辑源(上传一张或多张图片,也可以组合一张 Gallery 图片)并调用
/api/edits或/api/edits/from-gallery/{image_id} - 后端创建任务并以 multipart 调用上游
/v1/images/edits - 源图片和支持参数会被转发;多参考图会以重复的
image[]字段发给上游,不支持格式(如 SVG)会被拒绝 - 通过 SSE 推送进度;返回数据解码/下载、校验并落盘
- 编辑结果进入预览和 Gallery,沿用与生成一致的队列/历史/取消模型
- Python 3.11+
- FastAPI
- Granian
- aiohttp
- SQLite
- Pydantic v2
- SvelteKit
- TypeScript
- Tailwind CSS
LICENSE
README.md
Dockerfile
docker-compose.yml
.env.example
VERSION
requirements.txt
package.json
backend/
requirements-dev.txt
app/
main.py
api/
core/
schemas/
repositories/
integrations/
services/
tests/
frontend/
package.json
svelte.config.js
vite.config.ts
src/
routes/
lib/
api/
stores/
components/
utils/
deploy/
nginx.conf
images/
data/
需要以下条件之一:
- Python 3.11 或更新版本
- Docker
- Docker Compose
实际生成图像需要一个外部 GPT 兼容图像 API。
直接 docker run 暴露的是 FastAPI/Granian 进程,不经过 nginx。runtime 镜像会让 Granian 直接服务 /_app/immutable 下的公开 SvelteKit immutable assets;受保护的 Gallery 图片/缩略图/下载字节仍由后端鉴权后通过普通 FileResponse 返回。
docker build -t gpt-image-panel .
docker run -d --name gpt-image-panel \
-p 127.0.0.1:9090:9090 \
-v $(pwd)/images:/app/images \
-v $(pwd)/data:/app/data \
gpt-image-panel如果解析 python:3.11-slim 时 Docker Hub 超时,可以改用可访问的镜像源:
docker build \
--build-arg PYTHON_BASE_IMAGE=docker.m.daocloud.io/library/python:3.11-slim \
--build-arg NODE_BASE_IMAGE=docker.m.daocloud.io/library/node:24-alpine \
-t gpt-image-panel .Compose 会把 ghcr.io/z1rconium/gpt-image-linux:v1.0.3 作为单个 web 服务运行在 http://127.0.0.1:9090。当前版本没有独立 worker 容器;后台生成、Gallery 导出、R2 同步、缩略图、SSE 和 HTTP 请求都在 Granian/FastAPI 服务内运行。只有明确要换镜像 tag 时才覆盖 GPT_IMAGE_PANEL_IMAGE。
cp .env.example .env
# 按需修改 .env
# 默认必须设置 ACCESS_KEY,除非显式设置 ALLOW_UNAUTHENTICATED=true
docker-compose up -d --force-recreateGranian 要按应用容量调,不要只按 CPU 数量加 worker。生成吞吐优先保持 GRANIAN_WORKERS <= MAX_ACTIVE_GENERATE_JOBS;通常 2-4 个 worker 就够。GRANIAN_BACKPRESSURE 用来限制每个 worker 同时进入 Python 的慢请求数量。
GRANIAN_WORKERS>1 已支持生成/编辑 fan-out,以及可跟踪 Gallery export/import/R2 sync job:worker 通过 SQLite 认领任务,direct ZIP 下载共享 SQLite 里的 export 容量 slot,SSE 订阅使用 SQLite 全局 lease,启动维护只在同批 worker 中运行一次,GC/定时同步使用后台 leader lease,取消会标记未完成的生成 unit,但不会跨进程强杀正在等待上游的调用。
pip install -r backend/requirements-dev.txt
npm --prefix frontend install
npm run backend:dev另开一个终端:
npm run frontend:dev然后打开 http://localhost:5173。Vite dev server 会把 /api 和 /health 代理到 127.0.0.1:9090,浏览器侧仍然是同源路径。
默认只监听 127.0.0.1;如果要做局域网/外网调试,使用 npm run frontend:dev -- --host 0.0.0.0。
如果要单进程 smoke test,先构建前端,再通过 Granian 运行 FastAPI:
npm run frontend:build
granian --interface asgi backend.app.main:app --host 0.0.0.0 --port 9090 --reload然后打开 http://localhost:9090。
若本地开发需要无鉴权启动,请设置 ALLOW_UNAUTHENTICATED=true。该模式启动时会输出 warning,因为未设置 ACCESS_KEY 时所有非健康检查 API 都不需要鉴权。
curl http://localhost:9090/health- 打开网站
- 可用左上角语言按钮在英文/简体中文之间切换
- 点击右上角齿轮图标
- 选择已有预设,或点击 New 新建预设
- 填写 API Base URL
- 选择 API Path
- 填写该预设的默认模型;Generate/Edit 表单里的 Model 默认值会使用当前预设的值
- 选择该预设的默认 Response Format;Generate/Edit 表单里的 Response format 默认值会使用当前预设的值
- 填写 API Key,或填写
${OPENAI_API_KEY}这类环境变量引用;直接填写的 key 需要显式设置ALLOW_PLAINTEXT_SECRETS=true - 可选:填写全局 SOCKS5 代理,或填写
${UPSTREAM_PROXY_URL}这类环境变量引用 - 可选:填写全局 Webhook URL,或填写
${WEBHOOK_URL}这类环境变量引用,用于生成/编辑任务完成回调 - 可选:配置 R2 备份的 endpoint URL、储存桶、prefix,以及
${R2_ACCESS_KEY_ID}/${R2_SECRET_ACCESS_KEY}这类环境变量引用,然后点击测试 R2 - 可选:配置提示词优化器的 endpoint URL、模型、超时时间和 API Key/环境变量引用;直接填写的 key 需要显式设置
ALLOW_PLAINTEXT_SECRETS=true - 可选:打开 Overall Config,编辑主 Settings 表单里没有覆盖的
.env.example变量 - 可选:对已保存预设执行 Health check
- 点击 Save Preset
- 输入提示词
- 点击提示词助手标签追加常用修饰词
- 点击 Optimize 通过服务端优化器改写提示词
- 点击右上角提示词按钮保存或复用提示词片段;使用片段会替换当前提示词
- 选择生成参数;需要逐次复用不同上游路径时可直接选择 API Path
- 点击 Generate
- 也可以上传一张或多张编辑参考图、在 Gallery/Lightbox 中选择 “Edit this image”,或两者组合;上传会追加到当前编辑源,Clear 会清空全部编辑源
- 点击 Edits 执行图生图
- 在 Gallery/Lightbox 使用 “Use prompt” 或 “Use all” 复用历史提示词或完整参数
- 查看预览和 Gallery;点击 Gallery 的同步按钮可把本地 Gallery 中 R2 prefix 下缺失的图片上传到配置的储存桶
面板支持以下上游路径。API Base URL 可以不带 /v1,也可以带 /v1;例如 https://api.example.com 和 https://api.example.com/v1 都可以。
- 向 Images API 发送生成请求
- 从
data[]读取图片数据
- 向 Responses API 发送生成请求
- 上游请求体只发送
prompt和model;UI 里的模型默认值来自当前预设 - 从
output[]中类型为image_generation_call的项目读取 base64 图片数据 - 选择该路径时,界面中的尺寸、质量、格式、压缩率、数量和 response format 控件会被禁用
- 发送兼容 OpenAI Chat Completions 的生成请求
- 上游请求体只发送
model、messages和stream: false - 支持
grok-imagine-image-lite以及其他通过 chat completion 消息返回图片 URL 或 base64 数据的图像模型 - 可从 JSON chat completions 或
data:SSE chunk 中读取图片输出,包括这类 Markdown 图片链接 - 选择该路径时,界面中的尺寸、质量、格式、压缩率、数量和 response format 控件会被禁用
- 上传图片后点击 Edits、在 Gallery 里选择 “Edit this image” 后点击 Edits,或两者组合使用
- 始终在配置的 API Base URL 下调用
/v1/images/edits - 使用 multipart/form-data 发送源图片字段和支持的编辑参数;单张上传使用
image,多参考图会以重复的image[]字段转发给上游 - 最多支持 16 张编辑参考图;本地上传会追加到当前编辑源列表,并可与一张 Gallery 源图组合
- 上传的源图必须是受支持的位图图片格式;SVG 上传会被拒绝
- 如果上游返回
404、405或501,界面会提示/v1/images/edits不受支持并停止编辑请求
POST /api/settings/presets/{preset_id}/health会校验已保存预设,不会发送真实生成请求- 检查项包括 API Path 是否允许、HTTPS URL/hostname、上游 host allowlist、SSRF DNS/内网 IP 校验、API Key/环境变量引用是否可用,以及低成本
OPTIONS/HEAD上游探测 - 上游探测只作为诊断信息:
OPTIONS 404/410会显示为 warning,因为很多 GPT-compatible 网关只实现POST;如果只有upstream_probe是 error,整体预设健康状态仍显示正常 - 返回结构为
{ status, checks: [{ name, status, message }] },状态值为ok、warning或error - API Key 环境变量引用必须使用完整的
${ENV_VAR_NAME}格式;数据库只保存引用字符串,生成/编辑请求会在执行时从服务端环境变量解析真实值 - 默认拒绝直接把 API Key 明文持久化到 SQLite;只有显式设置
ALLOW_PLAINTEXT_SECRETS=true才允许
- Settings 里有
Overall Config弹窗,用来管理主 Settings 表单未覆盖的.env.example变量。 - 服务启动时会把当前进程环境变量同步到 SQLite。生效优先级是
SQLite override > 当前 env > 代码默认值。 - 弹窗保存的是 SQLite override,不会改写
.env文件;只要保留data/app.sqlite3,容器重建后 override 仍保留。 - Secret 保存时会把真实 override 写入 SQLite,但 API 响应和 UI 只返回打码值。提交
********会保留已有 secret override;Reset to .env会清除该 override。 - 可热生效字段会立即更新内存配置。运行路径、访问/session 启动行为、队列并发和 Docker build image 会标记为需重启或仅构建期生效。
DATABASE_FILE、DATA_DIR、IMAGES_DIR、THUMBNAILS_DIR可以显示和保存,但当前进程不能不重启就迁移 SQLite 连接或存储目录。
- Settings 抽屉提供一个全局
SOCKS5 代理字段,不跟随 API 预设切换。 - 留空时生成/编辑上游 API 请求保持直连。
- 默认使用
${UPSTREAM_PROXY_URL}这类环境变量引用。若直接填写socks5://host:port或socks5://user:pass@host:port,需要显式设置ALLOW_PLAINTEXT_SECRETS=true;保存后的代理密码会在 API 响应和 UI 中打码。 - 代理边界刻意收窄:只有生成/编辑的上游 API
POST请求会使用 SOCKS5。Preset health check、Webhook、版本检查、前端/api/*请求和上游返回的图片 URL 下载都保持直连。
- Settings 抽屉在
SOCKS5 代理下方提供一个全局Webhook URL字段,不跟随 API 预设切换。 - 留空时不发送任务回调。
- 默认使用
${WEBHOOK_URL}这类环境变量引用;若直接填写 webhook URL,需要显式设置ALLOW_PLAINTEXT_SECRETS=true。 - 配置后,生成/编辑任务完成时会向该 HTTPS URL 发送签名 webhook 回调。
- 配置 Webhook URL 时需要设置
WEBHOOK_SIGNING_SECRET;保存后的 webhook URL 会在 API 响应和 UI 中打码。
- R2 Backup 是增量备份路径,不是远端 Gallery 存储。
- 可以用
.env的R2_BACKUP_ENABLED=true加其他R2_*变量配置,也可以在 Web Settings 中保存配置。当 SQLite 里还没有保存过 R2 settings 时,启动会把当前.envR2 值持久化进去,所以 Web Settings 会直接显示这些值;之后 Web Settings 保存的值优先。Web Settings 的凭据字段支持${ENV_VAR_NAME}引用;直接保存明文凭据需要ALLOW_PLAINTEXT_SECRETS=true。 - Settings 里的测试 R2 会执行
HeadBucket、带 prefix 的ListObjectsV2,并写入一个很小的 probe object;probe 清理失败会作为 warning 返回。 - Gallery 的同步按钮会把确认上传的对象记录到 SQLite,通常只比较当前
R2_KEY_PREFIX下本地新增或变更的 filename;已存在对象会跳过,bucket 中额外对象不会删除或覆盖。远端被手动删除这类情况可用 full reconcile 模式重新核对本地 filename 和 R2。 R2_SYNC_CONCURRENCY控制有界并发的 R2HEAD/上传 worker;默认4,大量小图通常比完全串行同步更快。- 将
R2_SYNC_INTERVAL_HOURS或 Web Settings 里的同步间隔设为正整数,可按相同增量逻辑定时同步。0表示关闭自动同步,服务启动后会等待完整间隔再执行第一次定时同步。
auto— 默认值;让模型自动选择输出尺寸- 比例预设 — 1K / 2K / 4K,支持比例
1:1、4:3、3:4、16:9、9:16、21:9 - 自定义宽高 — 会归一化到 16 的倍数,最大边
3840px,最大纵横比3:1,像素总量在655360到8294400之间
- Quality:
auto、low、medium、high - Format:
PNG、JPEG、WebP - Compression:
PNG不可用;JPEG和WebP可设置0-100 - Quantity:
1到10;编辑时可以先清空,提交 Generate/Edit 时如果为空会自动回填为1 - Response Format:默认使用当前预设的值(默认
url),仍可选none和b64_json;none会省略response_format参数
- 每张上传的编辑源图大小受
MAX_FILE_SIZE_MB限制,必须通过 Pillow 完整解码校验和像素炸弹限制,且必须是受支持的位图格式(.png、.jpg、.jpeg、.webp、.gif、.avif、.bmp、.heic、.heif、.ico、.tif、.tiff);SVG 会被拒绝 /api/import只接受由/api/download-all导出的 ZIP 归档- 导入 ZIP 必须包含
metadata.json或metadata.ndjson;新导出会同时包含两者,导入优先读取metadata.ndjson,避免大图库 metadata 数组常驻内存,同时保留metadata.json给旧版本兼容 - 所选图片 ZIP 下载遇到缺失图库行或缺失图片文件时,会在
metadata.skipped中记录跳过项 - 导入 ZIP 会校验上传体积、文件数、解压总体积、metadata 大小、安全路径和压缩比
- 导入图片条目在存储前必须通过扩展名、Content-Type/文件魔数、完整解码和像素数量校验
.env.example 中列出的变量都会同步到 SQLite 的 Overall Config。主 Settings 表单已覆盖的变量仍在原位置管理;Overall Config 弹窗显示其余运行时、安全、限制和构建相关配置。
| 变量 | 默认值 | 说明 |
|---|---|---|
DEFAULT_API_URL |
空 | 预填 API Base URL;可以不带或带 /v1 |
DEFAULT_API_KEY |
空 | 预填 API Key;优先使用 ${OPENAI_API_KEY} 这类环境变量引用,直接填写的 key 会以明文保存到 SQLite |
DEFAULT_API_PATH |
/v1/images/generations |
默认上游路径;支持 /v1/images/generations、/v1/responses 和 /v1/chat/completions |
DEFAULT_RESPONSES_MODEL |
gpt-5.4 |
当请求/预设没有提供模型时,/v1/responses 使用的兜底顶层模型 |
DEFAULT_UPSTREAM_SOCKS5_PROXY |
空 | 可选的全局 SOCKS5 代理默认值,仅用于生成/编辑的上游 API 请求 |
AIOHTTP_CONNECTION_LIMIT |
100 |
上游/探测/下载 aiohttp connector 总连接数限制 |
AIOHTTP_CONNECTION_LIMIT_PER_HOST |
20 |
aiohttp connector 单 host 连接数限制;0 表示不限制单 host |
PROMPT_OPTIMIZER_ENABLED |
false |
是否启用服务端提示词优化器 |
PROMPT_OPTIMIZER_API_URL |
空 | 提示词优化器使用的完整 Chat Completions 兼容 endpoint URL |
PROMPT_OPTIMIZER_API_KEY |
空 | 提示词优化器 API Key;优先使用 ${OPENAI_API_KEY} 这类环境变量引用,直接填写的 key 会以明文保存到 SQLite |
PROMPT_OPTIMIZER_MODEL |
gpt-4o-mini |
提示词优化器模型 |
PROMPT_OPTIMIZER_TIMEOUT_SECONDS |
60 |
提示词优化请求超时时间(秒) |
PROMPT_OPTIMIZER_MAX_OUTPUT_CHARS |
4000 |
回填到文本框的优化后提示词最大长度 |
PROMPT_OPTIMIZER_MAX_RESPONSE_MB |
8 |
JSON 解析前允许的最大优化器上游响应体积(MB) |
PROMPT_OPTIMIZER_HOST_ALLOWLIST |
空 | 可选的优化器 endpoint 主机名白名单,逗号分隔 |
R2_BACKUP_ENABLED |
false |
使用环境变量配置 R2 时,启用 Gallery 同步备份 |
R2_ENDPOINT_URL |
空 | Cloudflare R2 S3 兼容 endpoint URL,例如 https://ACCOUNT_ID.r2.cloudflarestorage.com |
R2_BUCKET_NAME |
空 | Gallery 同步备份使用的储存桶 |
R2_REGION |
auto |
R2 使用的 S3 client region name |
R2_KEY_PREFIX |
gallery/ |
Gallery 备份对象 key prefix;手动验证建议使用 gallery-test/ 这类独立 prefix |
R2_SYNC_INTERVAL_HOURS |
0 |
Gallery Sync 定时同步间隔,单位小时;0 表示关闭自动同步 |
R2_SYNC_CONCURRENCY |
4 |
Gallery Sync 使用的并发 R2 HEAD/上传 worker 数 |
R2_ACCESS_KEY_ID |
空 | env-ref 凭据解析使用的 R2 access key ID |
R2_SECRET_ACCESS_KEY |
空 | env-ref 凭据解析使用的 R2 secret access key |
APP_VERSION |
VERSION 文件 |
覆盖界面显示和 /api/version 返回的当前应用版本;每次请求实时读取 |
GITHUB_REPO |
Z1rconium/gpt-image-linux |
用于检测 latest release 新版本的 GitHub owner/repo;设为空可禁用最新版本检查 |
ENABLE_VERSION_CHECK |
true |
启用每次请求的 GitHub 最新版本检测,用于 Header 的 New 标记 |
VERSION_CHECK_TIMEOUT_SECONDS |
3 |
每次请求 GitHub latest release 或分支 VERSION 的超时时间 |
VERSION_CHECK_BRANCH |
main |
latest release 失败后,用于回退读取 raw VERSION 文件的分支 |
ENABLE_METRICS |
false |
启用 /api/metrics JSON counters/gauges/rates/延迟摘要和 Prometheus 文本输出 |
SLOW_GALLERY_QUERY_MS |
200 |
/api/gallery 达到该阈值时记录筛选条件、页码、total、DB 查询耗时,以及 rows/count/total-bytes/filter-options 分段耗时 |
ENABLE_NGINX_ACCEL_REDIRECT |
false |
nginx internal alias 在前置时,为已授权的图片/缩略图/下载响应返回 X-Accel-Redirect;Compose 默认启用 |
PUBLIC_IMAGE_BASE_URL |
空 | 可选图片公开/CDN base URL;设置后 Gallery 和 job 的图片 URL 指向该地址,/api/image 仍保留为鉴权兜底 |
PUBLIC_THUMBNAIL_BASE_URL |
空 | 可选缩略图公开/CDN base URL;设置后 Gallery 缩略图 URL 指向该地址,/api/thumb 仍保留为鉴权兜底 |
ACCESS_KEY |
空 | 默认要求设置;设置后每个非健康路由均需解锁 |
ALLOW_UNAUTHENTICATED |
false |
设置为 true 可显式允许在未设置 ACCESS_KEY 时启动;启动时会输出 warning,因为非健康检查 API 不需要鉴权 |
ACCESS_KEY_COOKIE_NAME |
gpt_image_access |
访问会话使用的浏览器 cookie 名称 |
ACCESS_COOKIE_SECURE |
true |
给访问 cookie 添加 Secure;仅在纯 HTTP 本地/内网部署时设为 false |
ACCESS_MAX_FAILURES |
5 |
触发临时锁定前允许的访问密钥失败次数 |
ACCESS_LOCKOUT_SECONDS |
300 |
失败次数过多后的锁定时长(秒) |
IP_ALLOWLIST |
空 | 允许访问的 IP/CIDR,逗号分隔 |
TRUST_PROXY_HEADERS |
false |
是否读取受信任反向代理的 X-Forwarded-For、X-Real-IP、X-Forwarded-Proto 或 X-Forwarded-Host |
TRUSTED_PROXY_IPS |
空 | 允许提供可信代理头的代理 IP/CIDR,逗号分隔 |
PUBLIC_ORIGIN |
空 | 可选的浏览器侧规范 origin,例如 https://panel.example.com;同时加入 Host 白名单并作为 CSRF expected origin |
ALLOWED_HOSTS |
空 | 可选的 Host / 受信任 X-Forwarded-Host 白名单,逗号分隔;支持 hostname、host:port 或 origin |
CSRF_ORIGIN_CHECK_ENABLED |
true |
是否拒绝无法通过 Origin、Referer 或同源 Sec-Fetch-Site 证明来源的 POST、PATCH、DELETE 请求 |
UPSTREAM_HOST_ALLOWLIST |
空 | 生成/编辑上游 API URL 的可选主机名白名单,逗号分隔;配置 SOCKS5 上游代理时,代理本身是远端 DNS/网络可达性的信任边界 |
WEBHOOK_HOST_ALLOWLIST |
空 | 可选 webhook 主机名白名单,逗号分隔 |
MAX_FILE_SIZE_MB |
50 |
上传为编辑源图的图片、导入图片文件和上游图片 URL 下载的最大体积(MB) |
MAX_JSON_BODY_MB |
1 |
非上传 API 调用的最大 JSON 请求体积(MB) |
MAX_UPSTREAM_JSON_MB |
128 |
解析前允许的最大上游 JSON/SSE 响应体积(MB);大图或多图建议使用 response_format=url |
MAX_IMAGE_PIXELS |
100000000 |
Pillow 接受的最大解码图片像素数,超过后按 decompression bomb 拒绝 |
IMPORT_ARCHIVE_MAX_MB |
1000 |
/api/import 可上传 ZIP 的最大体积(MB) |
IMPORT_MAX_FILES |
500 |
单个导入归档允许的最大文件数 |
IMPORT_MAX_UNCOMPRESSED_MB |
1024 |
导入归档内所有文件解压后的最大总体积(MB) |
IMPORT_MAX_METADATA_BYTES |
2097152 |
导入归档中 metadata.json 的最大字节数 |
IMPORT_MAX_COMPRESSION_RATIO |
100 |
单个导入文件允许的最大解压/压缩体积比 |
GRANIAN_WORKERS |
1 |
Granian worker 进程数;生成吞吐建议保持 <= MAX_ACTIVE_GENERATE_JOBS |
GRANIAN_RUNTIME_THREADS |
2 |
每个 worker 的 Granian Rust runtime 线程数;多 worker + nginx/HTTP/1 时可先试 1 |
GRANIAN_LOOP |
uvloop |
Granian event loop 实现 |
GRANIAN_BACKPRESSURE |
100 |
每个 worker 的请求 backpressure;慢请求超过后不再继续进入 Python |
GRANIAN_BACKLOG |
2048 |
socket listen backlog,控制待接受连接积压 |
GRANIAN_STATIC_PATH_ROUTE |
/_app/immutable |
直接 Docker 模式下由 Granian 服务的公开 immutable asset 路由 |
GRANIAN_STATIC_PATH_MOUNT |
/app/frontend/build/_app/immutable |
直接 Docker 模式下挂给 Granian static 的 immutable asset 目录 |
GRANIAN_STATIC_PATH_EXPIRES |
31536000 |
Granian static asset 的 max-age 秒数 |
MAX_ACTIVE_GENERATE_JOBS |
2 |
全局允许同时运行的生成/编辑 image unit 数量 |
MAX_QUEUED_GENERATE_JOBS |
20 |
超出并发后允许继续排队的 image unit 数量;超过后新请求返回 429 |
IMAGE_JOB_UNIT_LEASE_SECONDS |
120 |
running image unit 的 SQLite claim lease,过期后其他 worker 可重试 |
IMAGE_JOB_UNIT_POLL_INTERVAL_SECONDS |
0.35 |
image-unit 调度和生成 SSE 的最小 SQLite 轮询间隔(秒);队列为空时调度器会退避 |
MAX_PENDING_EDIT_SOURCE_MB |
200 |
SQLite 全局预留的待处理编辑源图总字节上限(MB);设为 0 可关闭该字节上限 |
MAX_SSE_SUBSCRIBERS_GLOBAL |
200 |
通过 SQLite slot lease 跨所有 Granian worker 限制的最大活跃 SSE 订阅数 |
MAX_SSE_SUBSCRIBERS_PER_IP |
10 |
单个客户端 IP 允许的最大活跃 SSE 订阅数 |
SSE_CONNECTION_TTL_SECONDS |
3600 |
单条 SSE 连接的最长生命周期(秒) |
IMAGES_DIR |
./images |
图片存储目录 |
THUMBNAILS_DIR |
./images/thumbs |
Gallery 缩略图生成目录 |
THUMBNAIL_MAX_SIDE |
512 |
缩略图最大宽/高像素 |
THUMBNAIL_CPU_CONCURRENCY |
1 |
通过 SQLite 限制的跨 worker 全局缩略图生成并发 |
ALLOW_PLAINTEXT_SECRETS |
false |
是否允许把 API Key / SOCKS5 代理 URL / Webhook URL / R2 凭据明文持久化到 SQLite;默认要求 ${ENV_VAR_NAME} 引用 |
DATA_DIR |
./data |
SQLite 运行时数据目录;启动时会收紧到 0700 |
DATABASE_FILE |
./data/app.sqlite3 |
保存 Gallery 元数据和 API 预设的 SQLite 数据库;启动时会收紧到 0600 |
PYTHON_BASE_IMAGE |
python:3.11-slim |
Docker 构建基础镜像;Docker Hub 慢或不可访问时可覆盖 |
NODE_BASE_IMAGE |
node:24-alpine |
Docker 前端构建基础镜像;Docker Hub 慢或不可访问时可覆盖 |
WEBHOOK_SIGNING_SECRET |
空 | 配置全局 Webhook URL 时需要;用于签名 webhook payload(X-Webhook-Signature) |
WEBHOOK_TIMEOUT_SECONDS |
5 |
单次 webhook 投递超时时间(秒) |
WEBHOOK_MAX_ATTEMPTS |
3 |
webhook 最大重试次数 |
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/ |
前端页面 |
GET |
/health |
健康检查 |
GET |
/api/version |
当前应用版本、配置的 GitHub 仓库和 latest release URL;设置 ACCESS_KEY 时需要先解锁 |
GET |
/api/access/status |
访问密钥会话状态 |
POST |
/api/access |
解锁访问 3 小时 |
POST |
/api/settings |
保存当前 API 预设 |
GET |
/api/settings |
获取当前设置和预设列表 |
GET |
/api/settings/overall-config |
获取 Overall Config 字段、打码 secret、来源和需重启/仅构建元数据 |
PUT |
/api/settings/overall-config |
保存或清除 Overall Config 字段的 SQLite override |
POST |
/api/settings/r2/health |
校验草稿 R2 Backup 配置并执行 bucket/list/write 检查 |
POST |
/api/prompt/optimize |
通过服务端提示词优化器改写提示词 |
GET |
/api/prompt-snippets |
查询提示词片段,可选 query 筛选 |
POST |
/api/prompt-snippets |
创建提示词片段 |
PATCH |
/api/prompt-snippets/{snippet_id} |
更新提示词片段标题、内容或收藏标记 |
DELETE |
/api/prompt-snippets/{snippet_id} |
删除提示词片段 |
POST |
/api/settings/presets |
新建并激活 API 预设 |
POST |
/api/settings/presets/{preset_id}/activate |
激活 API 预设 |
POST |
/api/settings/presets/{preset_id}/health |
校验已保存 API 预设并执行低成本上游探测 |
DELETE |
/api/settings/presets/{preset_id} |
删除 API 预设 |
POST |
/api/generate |
创建图像生成任务 |
POST |
/api/edits |
使用一张或多张 multipart 上传图片创建图像编辑任务 |
POST |
/api/edits/from-gallery/{image_id} |
使用已有 Gallery 图片创建图像编辑任务,可附加上传参考图 |
GET |
/api/generate/jobs |
查询排队/运行中的生成和编辑任务;传 include_finished=true 并可选 limit/offset 可分页查询持久化历史,传 failed_only=true 仅返回 error/upstream_error 历史行 |
GET |
/api/generate/jobs/events |
通过 SSE 推送排队/运行中的生成和编辑任务 |
DELETE |
/api/generate/jobs/history |
删除全部已结束的生成/编辑任务历史记录;保留排队/运行任务和 Gallery 图片 |
GET |
/api/generate/{job_id} |
查询任务状态或结果 |
GET |
/api/generate/{job_id}/events |
通过 SSE 推送单个任务状态和进度 |
DELETE |
/api/generate/{job_id} |
取消并移除排队/运行中的生成或编辑任务 |
GET |
/api/gallery |
通过页码或 cursor 分页查询 Gallery 图片,可选 prompt、model、preset、size、date_from、date_to、favorite、include_total_bytes、include_counts、include_filter_options 筛选;图片行包含 thumbnail_status(ready、queued 或 missing) |
PATCH |
/api/gallery/{id}/favorite |
设置或取消 Gallery 收藏标记 |
GET |
/api/gallery/{image_id} |
按 ID 获取单个 Gallery 条目 |
GET |
/api/image/{filename} |
访问图片文件 |
GET |
/api/thumb/{filename} |
访问已存在的 WebP Gallery 缩略图;缺失时排入后台 worker |
GET |
/api/download/{filename} |
下载图片 |
DELETE |
/api/gallery/{id} |
删除 Gallery 条目和对应服务器图片文件 |
GET |
/api/download-all |
低内存流式下载 Gallery 所有图片和 metadata.json/metadata.ndjson 为 ZIP 文件;可传 direct export job 返回的 export_job_id |
POST |
/api/gallery/direct-export-jobs |
为完整 Gallery 预留 direct streaming ZIP 导出任务,并返回 download_url |
GET |
/api/gallery/direct-export-jobs/{job_id} |
查询 direct streaming ZIP 导出任务状态 |
GET |
/api/gallery/direct-export-jobs/{job_id}/events |
通过 SSE 推送 direct ZIP 准备/输出进度 |
POST |
/api/gallery/export-jobs |
创建可跟踪进度的 Gallery ZIP 导出任务;可传 ids 导出所选图片 |
GET |
/api/gallery/export-jobs/{job_id} |
查询 ZIP 导出任务状态 |
GET |
/api/gallery/export-jobs/{job_id}/events |
通过 SSE 推送 ZIP 打包进度 |
GET |
/api/gallery/export-jobs/{job_id}/download |
下载已完成的 ZIP 导出,并通过 Content-Length 支持传输进度 |
POST |
/api/gallery/sync-jobs |
创建单活 R2 Gallery 备份同步任务;支持 dry_run 和 full_reconcile |
GET |
/api/gallery/sync-jobs/{job_id} |
查询 R2 同步任务状态 |
GET |
/api/gallery/sync-jobs/{job_id}/events |
通过 SSE 推送 R2 同步进度 |
POST |
/api/import |
导入 /api/download-all 创建的 ZIP;传 async_job=true 可创建带进度的导入任务 |
GET |
/api/gallery/import-jobs/{job_id} |
查询可跟踪 Gallery 导入任务状态 |
GET |
/api/gallery/import-jobs/{job_id}/events |
通过 SSE 推送 Gallery 导入进度 |
DELETE |
/api/gallery |
删除所有 Gallery 条目和服务器图片文件 |
GET |
/api/metrics |
可选指标快照;默认 JSON,带 Accept: text/plain 时返回 Prometheus exposition format;仅在 ENABLE_METRICS=true 时可用 |
GET |
/api/metrics/prometheus |
可选 Prometheus exposition metrics;仅在 ENABLE_METRICS=true 时可用 |
- 版本读取顺序是
APP_VERSION->VERSION;本地版本和可选 GitHub 远端检查都会在访问解锁后、每次 Web 端触发版本请求时实时计算。远端检查会先读 latest release,再回退到配置分支的VERSION,仅用于显示New,不会阻塞使用。 - 配置
PUBLIC_ORIGIN或ALLOWED_HOSTS后,未知Host或受信任X-Forwarded-Host会在 CSRF 检查前被拒绝;有副作用的浏览器请求必须带Origin、Referer或同源Sec-Fetch-Site,且Sec-Fetch-Site: same-origin不能绕过 Host 白名单 - 上游返回的图片 URL 必须使用 HTTPS;后端发起下载前会拒绝 plain HTTP 图片 URL
- 配置上游 SOCKS5 代理时,后端仍会在连接前验证上游 URL,并在主机名本地解析到私有/内部 IP 时记录 warning;但远端 DNS 和网络可达性由 SOCKS5 代理决定,代理本身就是信任边界
- 预设、提示词收藏夹和 Gallery/Job 数据只保存在
DATABASE_FILE - SQLite 仓储操作使用短连接,并在启动时启用 WAL;
DATA_DIR会 chmod 为0700,SQLite 数据库和 sidecar 文件会 chmod 为0600;应用 shutdown 和测试 reset 会调用 storage close hook,连接生命周期保持显式 - 生成与编辑共用按 image units 计量的 SQLite 队列(
MAX_ACTIVE_GENERATE_JOBS+MAX_QUEUED_GENERATE_JOBS);入队容量检查、父任务创建、unit 插入和编辑源字节预留会在 SQLite 里原子提交;所有编辑源图先落到DATA_DIR/edit-sources,并额外受MAX_PENDING_EDIT_SOURCE_MB全局限制,任务终态/取消时释放;支持取消,并持久化终态历史(含completed_at) - 批量生成/编辑(
n > 1)会占用n个队列单位;父任务会把成功 unit 结果聚合到images[],Gallery 元数据保留用户请求的n,不同 unit 可被不同 Granian worker 认领执行 - 可跟踪 Gallery export job 和手动/定时 R2 sync job 持久化在 SQLite;
GRANIAN_WORKERS>1下创建、查询、SSE、下载都可跨进程工作;worker 通过 SQLite lease 认领 queued 或 lease 过期的 running job,空队列 dispatcher 会退避,GC/定时同步使用后台 leader lease,导出 ZIP 存在共享DATA_DIR/exports,direct ZIP 下载也会通过短生命周期 SQLite slot 行占用同一个全局 export 容量 - SSE 订阅上限通过 SQLite
sse_slots执行,所以MAX_SSE_SUBSCRIBERS_GLOBAL不再随 Granian worker 数量倍增;worker 异常退出时 stale slot 会过期释放 - Gallery 缩略图会在图片保存/import 和访问缺失缩略图时排队:
/api/thumb/{filename}只返回已存在文件或为缺失缩略图排队,Gallery 行返回thumbnail_status,后台 worker 按THUMBNAIL_CPU_CONCURRENCY全局 slot 限制运行 Pillow - 提示词优化器使用独立的服务端 Chat Completions 兼容 endpoint 配置和用户可配置请求超时/响应体积上限,在后端解析 API Key 环境变量引用,不占用生成/编辑任务队列容量。
- R2 Backup 通过 boto3 访问 Cloudflare R2 S3 兼容 endpoint,并用
asyncio.to_thread隔离阻塞调用;同步任务只列出配置的 prefix,只保留匹配本地 Gallery filename 的远端 key,远端 prefix 极大时会降级为按文件HeadObject检查,支持 dry-run 预检数量,记录每个文件的远端确认 metadata,只上传缺失的本地 Gallery filename,不会从 R2 服务、覆盖或删除 Gallery 图片。 - SSE 是主进度通道;
/api/generate/jobs提供列表/历史(include_finished=true,可选limit/offset,可选failed_only=true),/api/generate/jobs/history清空终态历史,/api/generate/jobs/events通过 SQLite 亚秒级轮询推送实时任务列表变化 - 任务终态历史包含
stage_timings:upstream_wait、download_decode、validate、db_insert;慢 Gallery 查询日志记录提示词是否存在/长度/hash、其他筛选条件、total、cursor/count flags,以及 rows/count/total-bytes/filter-options 分段耗时,并计入 metrics;可选 metrics 包含worker_id、最近 worker snapshots、队列深度、运行中任务数、失败率、任务分段耗时、SQLite busy/慢查询数、SSE poll 查询数、claim miss、活跃 SSE slots 和 worker heartbeat age;终态状态区分cancelled、interrupted和upstream_error,同时保留通用error - 上游 JSON/SSE 响应会在解析前受
MAX_UPSTREAM_JSON_MB限制,JSON 请求体受MAX_JSON_BODY_MB限制;上游图片 URL 下载会做 SSRF/重定向目标复核,并会经过 Pillow 完整解码、MAX_IMAGE_PIXELS像素限制和MAX_FILE_SIZE_MB体积限制 /api/import做 ZIP 安全与体积校验,并可通过async_job=true作为带进度的导入任务运行;/api/download-all保留低内存流式导出,并通过 SQLite-backed direct export job/slot 在传入export_job_id时用 SSE 汇报服务端准备/输出进度。完整 Gallery 导出默认使用 direct streaming。tracked export job 仍用于所选/小体积下载流程,会写入临时 ZIP 并借助Content-Length展示传输进度。ZIP metadata 会流式写入metadata.json和metadata.ndjson,使用已保存的sha256/bytes,导出时不会为了补缺失 hash 额外完整读取图片文件。- Gallery 持久化图片字节数和缩略图(
THUMBNAILS_DIR),旧图访问时会排队补缩略图,后台 file GC 会在短 TTL 后删除未被 SQLite 引用的孤儿图片/缩略图并保护仍被引用的 filename;使用 Compose 时,nginx 会直接返回 immutable 前端资源,并且只在 FastAPI 返回已授权的X-Accel-Redirect后发送图片/缩略图/下载文件字节;配置PUBLIC_IMAGE_BASE_URL/PUBLIC_THUMBNAIL_BASE_URL后,Gallery 和 job 响应可直接返回对象存储/CDN URL - 启动时会清理缺失文件对应的 Gallery 记录,并把上次进程遗留的 running/queued 任务标记为 interrupted
npm run frontend:check
npm run frontend:build
python3 -m pytest backend/tests/test_contract.py -q
npm run test:e2e
RUN_PERFORMANCE_TESTS=true python3 -m pytest backend/tests/test_performance.py -q
npm run test:e2e:perf契约测试覆盖冻结的公共 API 表面,包括访问 cookie、settings、generation/edit 任务创建、任务耗时指标、SSE 响应 framing、Gallery import/export、前端入口/CSP 处理、静态资源访问、下载、422 校验错误和 500 错误形状。Playwright 覆盖访问门禁、设置抽屉焦点行为、mock 生成/编辑流程、Gallery 筛选/页码跳转/批量操作、toast live region 和 Lightbox 键盘关闭/导航。
欢迎贡献。
建议遵循以下原则:
- 后端修改尽量保持简单和明确
- 用户可见变更或值得发布的修复应同步更新
VERSION,格式为vMAJOR.MINOR.PATCH - 尽量使用
backend/app/schemas/中的 FastAPI 响应模型 - 持久化存储操作集中在
backend/app/repositories/ - 上游 API 调用集中在
backend/app/integrations/ - 浏览器请求保持同源
/api/*路径,不要引入前端直连跨域后端 - 不要在仓库文件中保存真实 API Key
- 除非明确要求,否则不要提交生成图片或运行时 Gallery 元数据
- 除非明确要求改变任务生命周期,否则保留现有异步生成与 SSE 进度机制
本项目采用 CC BY-NC 4.0 许可证,即 Creative Commons Attribution-NonCommercial 4.0 International。
- 允许任何人使用、复制、修改、再分发以及二次创作。
- 需要保留署名,并附带许可证说明。
- 不允许将本项目或其衍生作品用于商业用途。
- 如需商业使用,必须事先获得著作权人的许可。
许可证全文见 LICENSE。