Imagen Content

A private image and video studio powered by OpenRouter. Create reusable profiles with a subject, visual style, things to avoid, and image/video defaults. Generate media, preview it, download it, and browse a searchable library.

Deployment

Production runs on the app server in /home/app/ImagenContent at https://app.tail60a042.ts.net:10007 (Tailscale only). Its .env sets IMAGEN_PORT=8093; Tailscale Serve proxies HTTPS port 10007 to 127.0.0.1:8093. Other services on the server are unchanged.

The 2026-10-17 migration preserved the running image as imagencontent-studio:migration-20261017, selected by compose.override.yaml. Before deploying future code changes on that server, rename that override to compose.override.yaml.migration-backup, then run docker compose up -d --build so the migration image override does not pin the old image. For routine restarts use docker compose restart studio.

The old devkit container is stopped and its HTTPS 9443 listener removed. Its volume remains available for rollback. Consistent migration backups (including secret.key) are stored at /tmp/opencode/imagen-migration-20261017/studio-data.tar.gz on devkit and /home/app/ImagenContent/studio-data-migration-20261017.tar.gz on app. These contain secrets; keep them private. Do not restart the old instance alongside production or restore an old backup over newer production data.

New installations

Requires Docker Compose and a machine already connected to Tailscale with HTTPS enabled.

docker compose up -d --build
tailscale serve --bg --https=9443 http://127.0.0.1:8092

Open the URL printed by Tailscale Serve while connected to Tailscale. Go to Settings, save an OpenRouter API key, create a profile, and generate an image or video. Set a spending limit on your key. Real generation costs OpenRouter credits.

The app binds only to host loopback, not the LAN or public internet. Tailscale Serve terminates HTTPS with a managed certificate. This does not enable Funnel or public access. Port 9443 is separate from existing Tailscale services on 443 and 8443. If your Tailscale ACLs restrict ports, allow 9443 for the intended users.

Compose uses the dedicated subnet 10.204.0.0/24 because this host's default Docker address pools are exhausted. On another machine, set IMAGEN_SUBNET in .env if that subnet conflicts with an existing network.

Alternatively, copy .env.example to .env and set OPENROUTER_API_KEY. Environment-managed keys cannot be changed through the UI. Never commit .env.

Features

  • App-style charcoal-and-sage workspace: fixed navigation, viewport-height panels, and internal scrolling instead of long pages. The studio keeps Generate pinned alongside a canvas and compact recent-media rail; mobile uses Compose / Canvas tabs and bottom navigation. Statistics have separate overview, models, profiles, and activity tabs.
  • Connection recovery with bounded request timeouts, safe read-only retries, a visible offline/reconnecting banner, and sequential background polling that pauses in hidden tabs. Drafts and selections remain intact. Mutations (especially paid generations) are never retried automatically when confirmation is lost.
  • Create, edit, delete, and reuse subject/style profiles for images and videos.
  • Upload PNG/JPEG/WebP images to the library, by file picker or drag-and-drop, without an API key or credit usage.
  • Save up to 8 reference images per profile; add scene-specific images and reuse uploaded or generated images.
  • Edit images with a prompt, and animate an uploaded/generated image as a video's starting frame.
  • Live OpenRouter image/video model discovery and model-aware ratio, resolution, and video duration controls. Video upscalers and SVG-only models are excluded. Editing and starting-frame workflows filter for compatible models.
  • Save models by OpenRouter ID, automatically fetching names, media types, descriptions, and capabilities; saved metadata remains available offline.
  • Dedicated Models page with provider pricing, complete metadata, search/type/status filters, persistent studio enable/disable controls, and daily refreshes.
  • Persistent generation queue; one job at a time, at most 10 queued/running jobs.
  • Combined prompt preview and immutable profile/prompt snapshots in generation history.
  • Image previews, video playback, downloads, search, filters, and media deletion.
  • Named galleries for uploaded and generated images/videos, with descriptions, bulk media selection, membership controls, and independent gallery pages. One item can belong to several galleries; deleting a gallery does not delete media.
  • Studio cost/statistics dashboard: recorded USD spend, model rankings, image/video requests and outputs, success rates, completion timing, profile usage, activity charts, and a CSV accounting export. Filter by 7/30/90 days or all time.
  • Server-side API calls; encrypted saved keys, same-origin mutation protection, and a restrictive content security policy.
  • Non-root, read-only Docker container with persistent SQLite/media storage and health checks.

Security and limitations

This is a single trusted workspace, not a multi-tenant service: there is no app login. Anyone permitted to reach it through Tailscale can view media, manage profiles/settings, and spend the configured key's credits. Restrict access using tailnet ACLs/grants. Local machine administrators can access the data and encryption key; encryption at rest does not defend against host compromise. Do not expose the loopback port through another public proxy.

Profiles combine prompt presets and optional reference images, not trained identities: exact character/subject consistency is model-dependent. Negative instructions are appended as “Avoid” text, not a separate provider-specific negative-prompt field. Model capabilities come from OpenRouter; individual providers may have additional restrictions. No custom model training or video editing is included.

Working with images

  • Library → Upload images: save your own pictures. Files can also be dropped into the library upload zone.
  • Create → Edit image: upload an image or choose From library, then describe what to change. Image models with reference support are shown. You can also open an image's library details and click Edit image.
  • Create → Video → Starting image: upload or select a first frame. Only models declaring first-frame support remain selectable. Library image details also offer Animate.
  • Profile editor → Saved reference images: upload or select subject/style references. These are automatically combined with scene references when that profile is used. Remove a saved reference using its thumbnail's × button.

Uploads are limited to 20 MB / 24 megapixels per image, and are decoded/re-encoded as PNG with orientation correction and metadata removal; SVG and other file types are rejected. The combined generation input limit is 40 MB, with at most 8 reference images and an optional video starting frame. Individual models can accept fewer references.

Reference images are sent server-side as base64 data URLs in input_references; video starting images use frame_images with first_frame. No public upload URLs or Tailscale Funnel are needed. Selected images leave this machine and are shared with OpenRouter/upstream providers when you generate, not when you merely upload. Providers may impose additional format/size constraints. Video reference-image support is not fully exposed in discovery metadata: inspect About this model and the provider documentation; some providers may ignore guidance references. A first frame and style/subject references serve different purposes.

Images attached to a saved profile or used by an active queued/running generation cannot be deleted until detached or finished. Finished jobs retain input filenames/profile snapshots, not permanent duplicate copies of source images. Deleting an unattached source image later can remove that image from historical details and exact reuse.

Galleries and statistics

Saved models

Open Models (or Create → Add / manage models) to see My models, including disabled or unsupported saved entries so they can always be removed. The studio starts with no automatically added models. Type in the search field or choose Search OpenRouter to browse OpenRouter image/video models, including unsupported workflows marked as unavailable for studio generation. Each card shows its ID, description, prices and billing units, capabilities, update times, and expandable complete provider data.

Use Add model by ID and enter only an OpenRouter model ID (provider/model). The app refreshes discovery and looks up the exact ID, saving its name, media type, description, capabilities, and available pricing. Unknown or unsupported IDs are rejected rather than inventing capabilities or prices. Saved entries are marked saved in selectors and persist in the Docker volume. Use Edit / refresh to fetch updated details, including for older manually configured entries.

Click + Add model on a search result to include it in your studio selection, or Added · Remove to remove it. Only explicitly added, supported models appear as studio/profile choices; unselected IDs are rejected server-side. A profile referencing a removed model remains intact but cannot generate with it. Jobs still waiting in the queue fail without an upstream generation request if their model is removed before execution; already-submitted requests finish normally. Selections apply to the whole trusted workspace and survive restarts. Previously saved models and explicit additions are retained; newly discovered models are never added automatically.

The server refreshes catalog data every 24 hours while running, checks on startup, and retries outages within an hour. Persistent snapshots avoid redundant requests after restarts. Image provider prices are fetched from the model’s endpoint metadata; video SKU prices and units are displayed as supplied by OpenRouter. Saved fetched metadata is refreshed too, without changing enable/disable choices. The open Models page rechecks data every minute; Refresh now forces an immediate update. Failed refreshes retain the last successful data, with stale/unknown price warnings. Prices depend on provider, resolution, duration, and billing units and are not a guaranteed per-generation quote. Metadata requests do not spend generation credits.

Browsing and generation validation use cached model metadata without waiting behind provider refreshes. Manual refreshes are bounded to 25 seconds per media type and keep the last successful data if OpenRouter is slow. Video thumbnails do not pre-download media metadata; opening a video loads it on demand.

This does not register/train a new model with OpenRouter or connect arbitrary provider URLs: the ID must already work on OpenRouter’s /images or /videos API. Saving requires discovery, but saved models remain selectable if discovery later fails. Removing a saved entry also removes it from your selection without deleting profiles, jobs, or accounting history. You can find and add it again through OpenRouter search.

The selected creative profile is remembered in this browser across reloads. New profiles default to the current studio model. If a profile’s model is unavailable or incompatible with attached images, the studio shows that model with a warning and blocks generation rather than silently selecting another model. You can explicitly choose another model for that creation.

Open Galleries → New gallery, give it a name/description, and use Add media to choose completed creations or uploads. The library and media details also offer Add to gallery, including creating a gallery on the spot. You can rename galleries, search within them, remove individual items, and delete a gallery without deleting the originals. Deleting media from the library removes its memberships from all galleries.

Open Statistics for this studio's accounting and usage, not your entire OpenRouter account. Costs are recorded from provider response usage.cost values. Missing costs remain unknown, including failed/interrupted requests for which no cost was reported; they are not assumed free. Uploads are free and excluded from model rankings and generation spend. The success rate excludes pending jobs. Completion timing includes queue time. Charts and date filters use UTC creation dates, not invoice dates; long all-time histories are grouped monthly.

A separate accounting ledger is automatically populated from existing jobs when the app starts. Deleting media retains minimal accounting metadata (model, profile label, timestamps, status, output count, recorded cost), so totals do not disappear when the library is cleaned up. Jobs deleted before this feature existed cannot be recovered. Previously unrecorded provider charges cannot be reconstructed either. This is a local usage report, not an authoritative invoice; confirm actual billing in OpenRouter. CSV export includes every record in the selected period; blank cost cells mean unknown. Profile rankings group saved profile labels, so renamed labels can appear separately.

Images use POST /api/v1/images. Videos use POST /api/v1/videos, status polling, and the authenticated content endpoint. Finished files are saved locally (250 MB maximum per asset). Videos are polled for up to one hour. Already-submitted video jobs resume after a restart. Interrupted image requests or video submissions without a persisted upstream ID are marked failed rather than automatically resubmitted, to avoid duplicate charges. Inspect OpenRouter activity before retrying such jobs. Deleting local media does not delete OpenRouter's records. No paid requests are made during setup or tests.

Operations

docker compose ps
docker compose logs --tail=100 studio
docker compose up -d --build  # update after code changes
docker compose down         # stop; retain stored data
tailscale serve --https=9443 off  # remove only this app's HTTPS listener

The commands above use the new-installation defaults. On production, the HTTPS listener is 10007, not 9443; use tailscale serve --https=10007 off only when intentionally taking production offline.

Data is in the Compose volume imagencontent_studio-data, mounted at /data: studio.sqlite, secret.key, and media/. Back up the entire volume, including the encryption key. Stop the container before a filesystem backup so SQLite/WAL are consistent. Do not use docker compose down -v unless you intend to erase all data.

Development and tests

Python 3.12+ and Node.js (for the JavaScript syntax check):

python -m venv .venv
.venv/bin/pip install -r requirements.txt pytest==9.1.1
.venv/bin/python -m pytest -q
node --check app/static/app.js
.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8092 --reload

Tests cover profile/gallery CRUD, gallery memberships and cleanup, cost aggregation and unknowns, date filters, CSV safety, retained accounting after deletion, encrypted secrets, CSRF protection, capability validation, uploads and limits, saved references, deletion protection, editing/first-frame payloads, queue bounds, media downloads/deletion, mocked image/video generation, error redaction, restart recovery, model enable/disable enforcement (including queued jobs), daily persistent pricing updates, and stale-data recovery. They use temporary storage and never spend credits.

Browser smoke tests also check the live HTTPS page and model discovery, desktop/mobile layout, and mocked settings/profile/generation/library flows. Install Playwright separately, install its Chromium browser, and run node tests/browser.cjs (or set NODE_PATH if installed outside the project). Set STUDIO_URL to test a different deployment. Browser mutations are intercepted, so these tests do not change live workspace data or spend credits.

Tech stack: FastAPI, SQLite, HTTPX, vanilla JavaScript/CSS. No frontend build step or external CDN assets.

S
Description
No description provided
Readme
349 KiB
Languages
JavaScript 41.9%
Python 35%
CSS 13.2%
HTML 9.7%
Dockerfile 0.2%