A personal workspace for making, comparing, and organizing AI images and video. Work from prompts and reference images, try several models at once, and keep the results together with the context that made them.
The working principle is immediate feedback: capture the idea, show the work in progress, and let the next idea follow. Image submissions show pending thumbnails before planning or rendering finishes, so you can keep prompting while earlier batches run in the background.
Send one or several prompts to multiple models, choose how many results you want from each, and compare the outputs side by side. Attach uploaded or generated images as references, with their order visible in the composer. The panel shows the output count and estimated cost before you generate.
Images is also the working library. Generate inside a group and the results land there; generate at the top level and they stay there. Hide the takes you do not want to look at, restore hidden images when needed, and use Trash for removal. Open an image at a larger size to judge it, inspect its details, or load its prompt and references back into the composer for another pass.
Type /storyboard directly in the image prompt field, followed by a scene idea.
Attach references for the subject, setting, or look you want to carry through.
For example:
/storyboard Six shots of this vehicle driving through an empty downtown.
Start wide, move closer, and finish beside a deserted fountain.
A shared plan establishes continuity and assigns each shot its own composition.
Every shot is generated as a separate full-size image using the original
references. The default is six 16:9 shots; request two through nine in the brief
or with --shots N.
The output count includes shots, models, and variants. After any batch confirmation, every shot gets its own numbered pending thumbnail immediately, and the composer remains available for more work. Results stay in the current group or at the top level.
If you later want a single sheet, select the images and use Reference sheet to assemble a downloadable composite.
Shots explores camera angles around a subject, using a reasoning model to inspect the actual subject and turn the chosen direction into rendering instructions. Outpaint reframes a finished image into other aspect ratios.
These tools create new images, keeping the source available for another attempt. Download individual results, a group, or a selection as a ZIP.
Generate a clip from a prompt, add a first or last frame, or supply reference images where the chosen model supports them. Image roles are explicit, and model compatibility, duration, shape, and resolution guide the available choices. Several prompts can produce several clips in one submission. The lineup is Seedance 2.5, Veo 3.1 Fast, Kling, MiniMax, LTX, and Flux; Seedance takes reference images or first/last frames, native audio, and clips up to 30 seconds. For models that support sound, Generate audio lets you choose audio or silent output.
Click a video thumbnail to open a large player with the complete frame, playback controls, and fullscreen support. First and last frame previews help you scan clips in the library. Continue takes a clip's ending as the starting image for the next generation and brings its prompt forward for editing.
Organize clips in video groups, hide takes while comparing results, and use Trash for removal. Generating inside a video group keeps the new clips there.
A session is a name and a run of clips, watched back to back — the question the whole page is for is whether the order cuts together and whether the next clip follows. A session can hold several cuts, shown as tabs, each a run of its own.
Add gen makes the next clip from inside the run: the last clip's final frame in the first slot, a prompt, a duration, one button. The pencil on a tile names a clip, or re-rolls it in place — a clip in the middle is pinned at both ends, so the joins either side survive. New cut from script remakes the open cut in one pass, from optional direction on style, pacing, or tone, each clip starting from the last one's final frame. A session can also start as a chat: ask a question and an invented character answers in short clips.
Director's clips belong to their session: they are not on the Video wall, they show in Activity for the cost record, and removing one from the run trashes it.
An edit is a timeline of clips off the Video wall. Add clips, drag them into order, and drag each tile's edges to set where it starts and stops -- the tiles are as wide as they are long, a ruler above them is the clock, and the cut plays straight through in the browser with nothing rendered. Space plays and pauses, Left and Right step a frame, S splits at the playhead, and F saves the frame on screen to a group on Images named after the edit, shown under the timeline too. Continue makes the clip between the highlighted clip and the next, pinned at both frames, and holds its place on the strip while FAL works. Export to Video cuts it into one clip on the Video wall.
News is an illustrated feed. A run has Claude search for recent image and video model news, compared against the models genzen already offers, and writes each story up as a post with a generated hero image. A post opens to its full write-up and sources.
Below tablet width the app has a phone layout. A green menu button sits bottom left: Images, Video, and More, which flies out with everything else. A green plus bottom right opens a one-tap composer on Images and Video. Walls are 2-up, and the image and video viewers fill the screen; swipe the image viewer to move through the set. Edit is desktop only.
Activity records generation prompts, references, model settings, timing, estimated cost, and failures. Storyboard runs retain the shared plan and each shot's rendering request. For images, Load restores an editable starting point, while Retry replays a failed request's saved inputs and settings.
Account summarizes recorded spend and output counts by model, alongside connection status, appearance settings, and keyboard shortcuts. Costs are estimates, not invoices: provider billing can differ, and AI planning can add usage beyond the image estimate shown in the composer.
Lab is where ideas, model capabilities, and workflows are being worked out and experimented with. Its tools can change as we learn what is useful; the main workspaces are where established workflows live.
GenZen is a personal creative workspace, with real accounts and per-user isolation but no public signup, teams, or sharing. Run it locally or deploy your own instance; generations bill the provider accounts whose keys you configure.
Postgres and media storage run locally with Docker. FAL provides image and video generation, and Anthropic provides the reasoning and vision used by AI-assisted workflows such as storyboards. The app can start without provider keys; the features that call those providers need their keys configured.
You need Docker, pnpm, and Node 22.13+. Postgres and media storage run in local containers; the app handles authentication.
The Node floor is not cosmetic: packageManager pins pnpm 11, which imports
node:sqlite and cannot run on Node 20. Corepack fetches pnpm before anything
reads engines, so an older Node fails during pnpm install with
ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite and no mention of your Node version.
A FAL_KEY is optional to start — the app runs without one, but image and video
generation need it. Supplying one means generations bill your fal.ai account;
nothing is mocked.
pnpm install
pnpm local:up # asks for your API keys, sets up everything else
pnpm dev # http://localhost:3000That is the whole setup — there is no global CLI to install and no env file to
copy or edit. local:up
starts Postgres and MinIO (S3-compatible storage) from docker-compose.yml,
writes .env.local for you, applies any migrations the database has not seen,
generates and provisions a login, and prompts for your FAL and optional
Anthropic keys. Re-run it any time: it is idempotent, it keeps your key, and it never resets a database you
have been working in. pnpm local:reset is the deliberate way to start over.
| Thing | Where |
|---|---|
| App | http://localhost:3000 |
| Sign in as | printed by local:up, kept in .env.local |
| MinIO console | http://localhost:9011 (genzenlocal/genzenlocal) |
| Postgres | postgres://genzen:genzen@localhost:5434/genzen |
There is no shipped account. local:up generates a password on first run,
creates the user, and prints the login; it lands in .env.local as
LOCAL_DEV_EMAIL / LOCAL_DEV_PASSWORD. Edit either one and re-run to change
it — the file is the source of truth and the password is re-synced from it,
which is also the whole password-reset story. pnpm users manages accounts on a
deployed instance — list, add, delete — and takes --local to work on the
docker stack instead.
FAL is not mocked — generation calls fal.ai for real and costs real money. The app boots and everything else works without a key. Generation cost estimates are recorded in the Activity log.
Ports: MinIO is on 9010/9011 rather than its default 9000/9001, so this stack can run alongside another local one holding MinIO's defaults.
If a shell-exported FAL_KEY shadows the one in .env.local, local:up warns
about it — that's the usual reason generation 401s.
| Command | Purpose |
|---|---|
pnpm local:up |
Start the local stack, write .env.local |
pnpm local:down |
Stop it (data kept) |
pnpm local:reset |
Stop it and delete the volumes |
pnpm dev |
Next dev server on :3000 |
pnpm build |
Production build |
pnpm test |
Vitest |
pnpm check |
Prettier + ESLint --fix + color, token and class checks (run before commit) |
pnpm check:colors |
Fail on a raw color outside tokens.css |
pnpm check:tokens |
Fail on a var(--x) that is declared nowhere |
pnpm check:classes |
Fail on a CSS module class that is used but never defined |
pnpm typecheck |
tsc --noEmit (the build typechecks too) |
pnpm db:migrate |
Apply pending migrations/*.sql |
pnpm users |
List/add/delete logins; -h for usage, --local for docker. Reaching a deployed database needs an authenticated Railway CLI |
pnpm check:claude-md |
What the pre-commit hook checks (advisory) |
pnpm activity:inspect |
Load a pasted Activity URL's stored data and media, no browser needed |
pnpm context:find |
Find uploads/generations by recency, local date or text |
pnpm context:inspect |
Load a generation's stored data and media by URL or id |
pnpm rive:build |
Rebuild the News progress animation from rive/news-progress/ |
| Layer | Tech |
|---|---|
| App | Next.js App Router (React 19 + Turbopack) |
| UI | CSS Modules + Base UI, on the tokens in src/styles/ |
| Data | Postgres, queried with SQL via postgres (no ORM) |
| Auth | scrypt + signed session cookie, own users table |
| Storage | S3 — MinIO locally, a Railway bucket in production |
| Images/video | FAL |
| Text/vision | Anthropic — prompt work, and vision |
Checked by src/lib/repo-map.test.ts — a path named here that does not exist
fails the build.
| Path | What's there |
|---|---|
src/features/<name>/ |
Domain modules. Each has its own CLAUDE.md — read it before editing the feature. |
src/lib/server/ |
.server.ts = never client-importable; .action.ts = a 'use server' module. |
src/components/ |
Primitives, one folder each, imported from the root barrel #/components. |
app/api/ |
Route handlers (app/api/auth/sign-out/). |
migrations/ |
Numbered SQL migrations, applied by pnpm db:migrate. |
docs/SPEC.md |
What the app does and the rules that must hold. |
docs/OVERVIEW.md |
What genzen is, and what it deliberately is not. |
docs/DELTAS.md |
genzen's deltas from project-standard. |
Locally there is nothing to configure. pnpm local:up writes .env.local
itself and prompts you for the values that are actually yours: your FAL key,
and your Anthropic key if you want the AI-assisted features. The app runs
without the second.
.env.example is the reference for deploying, split into Required (a Postgres
URL, a session secret, FAL, an S3 bucket) and Optional (Anthropic).
docs/deploying.md covers the rest: what a
deployment needs, the two non-default settings, and how the first user is made.
The R2_* names are historical and are staying that way (#242). The storage
layer is plain S3 pointing wherever R2_ENDPOINT says — MinIO locally, a
Railway bucket in production, Cloudflare R2 nowhere. The bucket must be
private (#226); the app serves images itself. R2_ACCOUNT_ID derives
Cloudflare's endpoint and is unused.
Provider keys are server-only. Only NEXT_PUBLIC_* reaches the browser — Next
inlines nothing else, and the VITE_ prefix carries no meaning here (#225).
- Route protection is deny-by-default in
proxy.ts— a new public path must be listed in itsPUBLIC_PATHS. - No Tailwind and no CSS framework.
src/styles/tokens.cssis the token layer,src/styles/base.cssthe reset; everything else is a.module.cssbeside its component.src/styles.cssimports those two and nothing else. Colors live intokens.cssalone —pnpm check:colorsenforces it (#229), andpnpm check:tokensfails on avar(--x)declared nowhere (#407). The second matters because an undeclared property does not error, it is dropped, so it breaks silently. .server.tsmust never be imported from client code;.action.tsis a'use server'module meant to be. Lint enforces the split (#241).- FAL generation status is reconciled via on-demand polling in
src/lib/server/generations/check-pending.action.ts. There are no webhooks — the route, the flag and the env vars went in #362, and polling is the only path by which a result reaches the app. - The bucket is private, so there are no public object URLs to persist. Images
are served by the app at
/img/[id], which resolves identity from the cookie and filters the row byuser_id.src/lib/image-url.tsis the only place a URL is built, and it returns an app path, never a storage key (#226). - Image batches create optimistic cards before preparation starts, then reconcile them with saved generation rows. Preparation and submission failures remain visible instead of silently dropping the request.
Focus: nothing in the Focus column. Open #748 (top of Now) -- Director reordering after Extract or a storyboard write fails as "changed in another tab".
Recent highlights:
- Phone nav and swipe (#768-#770, #764-#767) -- a green menu button bottom left (Images / Video / More, More flying out right), the plus bottom right. The image viewer swipes with a slide; fixed page scroll freezing after closing it, and blank images after a swipe. Hiding video controls on play was tried and reverted (#771): it stopped iOS playing at all.
- Mobile viewers (#762) -- Images and Video fill the screen, with title above, navigation below, and confirmation before Trash. Tap empty space to close; image prompts and actions live in a details sheet.
- Code tidy (#758, #757) -- knip's dead code removed, the S3 client
guarded
server-only;src/lib/server/grouped intofal/ claude/ storage/ media/ generations/, single-use files moved to their route. - Phone layout (#753, #755) -- below 48rem: a nav (now the corner menu, #768); on Images and Video a floating plus opens a one-tap composer (prompt, chips, Generate with cost), 2-up bare walls, a Photos-first picker; the Images viewer has Generate from this and Animate. Desktop unchanged.
- Director cuts (#744, #746, #747, #749, #751) -- a run session holds several cuts as tabs. New cut from script remakes the open cut in one pass, clips chained through durable frame handoffs; reference clips can try Veo Fast.
- Director chat answers end on a last beat (#740, #741) -- a tease, an offer, a fork or a provocation that invites the next question, never a question about the person and never a tidy wrap-up. Its own prompt section.

