Upload a few photos of yourself, pick a facial expression, and get photorealistic portraits of you performing it.
Expression Studio is an identity-preserving image generator. Your photos supply the
identity; a chosen expression reference (or your own text description) supplies the
expression; and the model renders fresh portraits that keep your likeness while
adopting the new expression. It's built on Nano Banana Pro (Google
gemini-3-pro-image), accessed through OpenRouter.
Expression Studio is a small, self-hosted web app for generating photorealistic
portraits of yourself in a chosen emotion. You bring your own reference photos and
your own OpenRouter API key; the app pairs your likeness
with one of 25 hand-tuned expression references (or a free-text description you
write) and asks Nano Banana Pro (Google gemini-3-pro-image) to render new,
identity-preserving portraits.
It's handy for things like profile-picture sets, avatars, expression sheets for illustration or reference, and mood variations of a headshot — all from a handful of photos. It is not a face-swap or deepfake tool for impersonating other people: the intended use is generating images of yourself, from photos you have the right to use. See the License & usage terms section for what that means in practice.
- 🎭 25 built-in expressions — from happy and shocked to skeptical, flirty, and pondering, each described in muscle-level detail for accurate results.
- ✍️ Describe your own — skip the presets and type a free-text expression; identity is always preserved.
- 🖼️ 1–4 variants per generation — pick how many portraits to render at once, then regenerate for a fresh set.
- 👤 Multi-photo identity — upload up to 5 reference photos (multiple angles improve likeness). Images are downscaled in the browser before upload.
- 🪟 Transparent background — optionally run server-side segmentation to get cut-out PNGs with a real alpha channel.
- 🧠 Headroom safety net — a post-generation pass re-frames portraits whose heads sit too close to the top edge, without distorting the subject.
- 🔒 Privacy-conscious — your API key stays server-side; photos are sent only to your own server and to OpenRouter.
| Pick an expression | Describe your own | Results |
|---|---|---|
![]() |
![]() |
![]() |
Browser (page.tsx)
└─ POST /api/generate
{ images: [dataURL...], count, transparent,
expression: "shocked" // preset mode
| customPrompt: "..." // custom mode (no reference image)
}
└─ route.ts (Node runtime)
├─ preset: reads facialExpressions/<expression>.png as the reference
├─ custom: no reference; uses the free-text prompt directly
└─ lib/imageGen.ts → `count` parallel OpenRouter chat-completion calls
model: google/gemini-3-pro-image, modalities: ["image","text"]
prompt: "images 1..n are the SAME person — keep their identity;
(preset) the LAST image is an expression reference —
copy ONLY the expression, not identity"
This is identity-preserving generation, not pixel-level compositing. The model renders new portraits rather than pasting an expression onto your photo.
- Node.js 18.17 or newer (Node 20 LTS recommended). Check with
node -v. - An OpenRouter API key whose account has access
to image models (
google/gemini-3-pro-image). - An internet connection for the first install — the native image dependencies
(
sharp,onnxruntime-node) download prebuilt binaries during install.
# HTTPS
git clone https://github.com/SonapSav/ExpressionStudio.git
# or SSH
git clone git@github.com:SonapSav/ExpressionStudio.git
cd ExpressionStudioThe steps are the same on Windows, macOS, and Linux — npm downloads the prebuilt native binaries for your platform automatically.
npm installmacOS / Linux: no extra steps. Windows: no extra steps either — just make sure you're on Node 18.17+. If a native module (
sharp,onnxruntime-node) ever fails to install, it's almost always a missing internet connection during install — reconnect and re-runnpm install.
Copy the example env file and add your OpenRouter key:
# macOS / Linux
cp .env.example .env.local
# Windows (PowerShell)
Copy-Item .env.example .env.localThen edit .env.local:
OPENROUTER_API_KEY=your_openrouter_api_key_here
npm run devOpen http://localhost:3011.
For a production build:
npm run build
npm run start- Upload a clear, front-facing photo (up to 5; multiple angles improve likeness).
- Pick an expression from the grid, or switch to Describe your own and type a free-text description.
- Choose how many variants (1–4) you want.
- (Optional) Tick Transparent background for cut-out PNGs with a real alpha channel instead of a photographic background.
- Generate — variants arrive in ~15–30s (a few seconds longer with a transparent background). Download any of them, or Regenerate for a fresh set.
The first transparent-background generation downloads the segmentation model weights (~40 MB, one time).
All configuration lives in .env.local (see .env.example):
| Variable | Required | Description |
|---|---|---|
OPENROUTER_API_KEY |
✅ | Your OpenRouter key with image-model access. |
OPENROUTER_IMAGE_MODEL |
Override the image model (default google/gemini-3-pro-image). |
|
OPENROUTER_IMAGE_RESOLUTION |
Force output resolution as WIDTHxHEIGHT (e.g. 1024x1280 for a 4:5 portrait). Also drives the requested aspect ratio. |
|
OPENROUTER_SITE_URL |
Site URL sent as HTTP-Referer for OpenRouter rankings. |
Edit lib/expressions.ts. Each entry needs a file present in both
facialExpressions/ (server-side reference) and public/expressions/ (served to
the browser grid), plus a prompt describing the expression in muscle-level detail.
| Path | Purpose |
|---|---|
app/page.tsx |
Main UI. |
app/api/generate/route.ts |
Validates the request, loads the reference PNG, returns variants. |
lib/expressions.ts |
The 25 expressions and their text descriptions. |
lib/imageGen.ts |
OpenRouter request + prompt construction (generateVariants). |
lib/removeBackground.ts |
Server-side background removal (makeTransparent). |
lib/headroom.ts |
Post-generation reframing safety net (ensureHeadroom). |
components/ |
PhotoUploader, ExpressionGrid, ResultsGrid. |
facialExpressions/ |
Source reference PNGs (server-side). |
public/expressions/ |
The same PNGs, served to the browser grid. |
- Cost: each "Generate" makes one image-generation call per variant, billed through your OpenRouter account. Regenerate makes another batch.
- Watermark: outputs carry Google's invisible SynthID watermark.
- Transparent background: Gemini image models can't emit alpha, so this option
runs a segmentation model (
@imgly/background-removal-node) on the server to cut the subject out. The native deps (onnxruntime-node,sharp) are marked as server-external innext.config.mjs. - Privacy: the API key stays server-side; photos are sent only to your server and to OpenRouter (which routes to Google for generation).
- Next.js 14 (App Router) + React 18 + TypeScript
- OpenRouter → Google
gemini-3-pro-image(Nano Banana Pro) sharpfor image processing@imgly/background-removal-node(ONNX Runtime) for transparent cut-outs
Expression Studio's own source code is released under the MIT License — see LICENSE. MIT © 2026 Panos Vasilopoulos.
The MIT license covers this application's code only. It does not grant any rights over the AI model, the images the model generates, or the third-party services this app depends on. Those are governed by separate terms that you are responsible for reviewing and complying with:
- The model is proprietary to Google. Generating images with it is subject to Google's generative-AI terms and usage policies, and to OpenRouter's terms — because every request is routed through your OpenRouter account and, from there, to Google.
- Commercial use. Neither this app nor its author grants you any rights to the model or to the images it produces. Whether you may use generated images commercially depends entirely on Google's and OpenRouter's current terms for the model and plan you use. Review them before any paid or commercial use:
- SynthID watermark. Every generated image carries Google's invisible SynthID watermark identifying it as AI-generated. Do not present outputs as unedited photographs.
- Likeness & consent. This app creates realistic images of real people from their photos. You are responsible for having the right to use every photo you upload and for complying with applicable law (consent, publicity/likeness rights, and data protection) — particularly for images of anyone other than yourself.
This section points you to the relevant terms; it is not legal advice. Terms change — when in doubt, consult the current Google and OpenRouter terms, and a lawyer for commercial use.



