A collaborative storyboard: planners, artists, and a director open the same board at the same time, break a script into cuts, generate the art, mark up the drawings with feedback, and sign off.
Runs on AWS. The drawings are generated by an open-weight model on our own GPU, not by a managed AWS model.
- Try it: Run locally needs no AWS account. Deploy is one
cdk deployplus one script for the accounts. - The accounts that script creates:
u1Kim Hana (planner) ·u2Lee Dohyun (artist) ·u3Park Seojun (director) ·u4Choi Yujin (reviewer) ·u5Jung Mina (admin). You choose the password at deploy time — it is never stored in this repository. - Screens:
walkthrough/
| Area | Capability |
|---|---|
| Concurrent editing | Many people edit one board. Merging is per field, so nobody overwrites your sentence. You see other people's cursors, avatars, and "editing now" markers |
| Planning | Write the story from one prompt (Amazon Bedrock, Claude Sonnet): it invents the characters, the synopsis and the cut-by-cut flow. Options: new story / next episode / spin-off, genre, tone, runtime, cut count, how many characters to invent, whether to reuse the board's cast. You read the outline first, then it lands on the board. Or paste a script you already have and split it into cuts mechanically |
| Episodes | One board holds the main story plus its next episodes and spin-offs. Cuts belong to an episode; characters are shared across all of them |
| Characters | One character, many poses (front, 3/4, side, back, full body, expression). Pick one as the reference and the other poses follow that face |
| Art | Generate from a prompt, upload a hand sketch, or transform an existing drawing. Pick one of three open-weight models per shot. Versions stack up, can be compared side by side, and can be deleted |
| Roles | Five: planner, artist, director, reviewer, admin. Making art — generate, sketch upload, model swap — is for artists and planners. Story planning is for planners and directors. Approval is the director's, and so is the 관리 tab (assignment table, team load). Changing someone's role is the admin's. Three of those are enforced on the server (generation, planning, role changes); everything else is UI only (see the disclaimer) |
| Review | Pin notes and circle marks on the drawing, tags (framing, light, background, wardrobe, expression, proportion), @mentions, replies, resolve |
| Approval | Draft → in progress → in review → approved / changes requested. Buttons are locked for roles that don't own the action |
| Notifications | A director's change request notifies the assignee and hands the cut back to them |
| Output | Timeline (duration per scene), print / PDF |
Browser ──┬─ CloudFront ─ S3 static files, no build step
├─ Cognito login; name and role come from token claims
├─ AppSync ─ DynamoDB op log + realtime subscription (WebSocket)
├─ AppSync ─ Bedrock Converse story planning — the one call that skips the op log
└─ CloudFront /gen ─ ALB ─ EC2 g6e (L40S) ─ open-weight model ─ S3
Tech: plain ES modules in the browser (no framework, no bundler) · Amazon Cognito user pool
(HTTP calls, no SDK) · AWS AppSync GraphQL with JS resolvers · DynamoDB as an append-only log ·
Amazon Bedrock (Claude Sonnet, called straight from an AppSync HTTP data source — no Lambda) ·
CloudFront + S3 · one EC2 g6e.2xlarge running FastAPI + PyTorch + 🤗 diffusers ·
Amazon Translate for Korean prompts · AWS CDK (JavaScript) for all of it.
Story planning is two calls, not one. The outline (title, logline, synopsis, characters, beats)
comes back first and a person reads it; only then are the beats expanded into cuts, four beats per
call, in parallel. One call for a 24-cut board would sit near the resolver's time limit and lose
everything if it broke. The prompts live in the browser (demo/story.js) so changing the wording
isn't a resolver redeploy, and the model's JSON is clipped and whitelisted by normalizePlan()
before any of it becomes an op — the model is a second trust boundary, not a trusted author.
Three models, one GPU. Pick per shot. All three are Apache-2.0, so an organization can deploy them.
| Model | Good for | Reference image | |
|---|---|---|---|
chroma |
Chroma1-Flash 8.9B | the default: 12 steps, ~15 s, the best pencil-storyboard texture | img2img — keeps the layout, redraws the face |
klein |
FLUX.2 klein 4B | same character in a new shot; 8 steps, ~4 s | condition — keeps the face |
hd |
Chroma1-HD 8.9B | Flash before distillation: 26 steps, for a final pass | img2img — keeps the layout, redraws the face |
The reference-image column is the reason there are three. chroma/hd paint over the image with
noise, so the layout survives and the face does not. klein takes it as a condition, so the face
survives — that's what makes "one character, many shots" work.
Only one of them fits in the card's 44 GB at a time, so picking a model swaps it: the server loads
the new weights in the background and refuses generation until they're resident (~1 min from disk).
The picker shows the wait. chroma is what a fresh deploy starts with.
The only truth on the board is the op log. One edit is one immutable op; on boot the client replays the log to build the screen. There is no separate stored state, which is why two people's edits can always be merged.
- Ordering uses fractional indexing, so simultaneous reorders don't corrupt the sequence.
- Same field: last write wins, per field. Different fields: both survive.
- Ops from other people must pass
scrub()indemo/core.jsbefore they touch state. That is the trust boundary.
demo/ browser. no framework, no build. upload this directory to S3 and you're done
index.html markup and all CSS
app.js every screen and interaction: board, detail, viewer, markup, timeline, print
core.js state rules — status transitions, role permissions, field merge, op validation,
plan normalization. pure functions, testable without a browser
story.js story planning: the option set, the prompts, and the two-phase Bedrock call.
falls back to splitting the prompt when there is no model (local mode)
net.js transport. AppSync when configured, BroadcastChannel between tabs when not.
app.js cannot tell which one it got
auth.js Cognito login and token refresh — two HTTP calls, no SDK
login.js login screen
art.js canvas-drawn placeholder art for local mode
dom.js the only place that writes HTML into the DOM, plus escaping
seed-art.js generated by scripts/seed-art.mjs; the starter board's images
aws-config.js `null` in the repo (that's what selects local mode); CDK overwrites it at deploy
test.html core.js self-check — open it in a browser and it prints PASS/FAIL
infra/
bin/app.js CDK entry point
lib/storyboard-stack.js the whole stack, one file
schema.graphql Op and Presence types, four queries/mutations, two subscriptions
resolvers/ AppSync JS resolvers — putOp, listOps, presence, plan (Bedrock Converse)
gpu/server.py FastAPI generation server: the three-model registry, one-resident-at-a-time
VRAM swapping, prompt assembly per model family, upload to S3
gpu/user-data.sh GPU boot script — pinned Python deps, systemd unit, model pre-download
scripts/users.sh create the five demo accounts
scripts/seed-art.mjs generate the starter board's images with the real model
python3 -m http.server 8000 --directory demo
# http://localhost:8000 — no login, and multiple tabs sync with each other liveWith an empty demo/aws-config.js the app runs in local mode (BroadcastChannel + localStorage) and
draws placeholder art instead of calling a GPU. Good enough to see concurrent editing work.
demo/seed-art.js ships empty, so the starter board draws canvas placeholders — the same locally and
on a fresh deployment. scripts/seed-art.mjs fills it with real generated art once a GPU is up, but
those /img/<hash>.png paths only resolve in the account that generated them. That is why the file is
committed empty: any account gets the same board on the first boot.
Two commands: cdk deploy builds everything, scripts/users.sh creates the accounts. Four things
have to be true before the first one — none of them are things CDK can do for you.
- Region
us-east-1. Not a preference. The stack is pinned there in three places: the CloudFront origin-facing prefix list ID (pl-3b927c52— different in every region), theg6eAZ list, and theus.anthropic.claude-sonnet-5inference profile (US regions only). Elsewhere means editinginfra/lib/storyboard-stack.js. - A default VPC in that region. The stack looks one up rather than creating one.
- Bedrock model access for Anthropic Claude, enabled once per account on the Bedrock console's
Model access page. Story planning returns
AccessDeniedExceptionuntil it is. Check withaws bedrock get-foundation-model-availability --region us-east-1 --model-id anthropic.claude-sonnet-5— you want"authorizationStatus": "AUTHORIZED". - At least 8 G-instance vCPUs.
g6e.2xlargeneeds 8 under Running On-Demand G and VT instances (L-DB2E81BA); a new account can start at 0. The stack still deploys — the ASG just records a failed scaling activity and no GPU ever appears. Request the increase first.
Then npx cdk bootstrap once per account and region. cdk.context.json is not committed, so the
VPC lookup runs again on your first synth.
cd infra && npm ci
npx cdk deploy --outputs-file /tmp/sb-out.json # prints Url, UserPoolId, ClientId, GpuAsg
POOL=$(node -p "require('/tmp/sb-out.json').StoryboardDemo.UserPoolId")
SB_PW='<pick a password>' bash scripts/users.sh "$POOL" # the five demo accounts
SB_PW='<the same password>' node scripts/seed-art.mjs # optional: real art for the starter board
npx cdk deploy # only if you ran seed-art: it writes a local fileThe password only ever arrives through SB_PW, never as an argument and never in a file — arguments
show up in shell history and ps. Leave SB_DEMO_PW unset: it prefills the login form for a private
demo, and setting it writes the password into aws-config.js, which anyone can read over CloudFront.
The GPU is only warm after it has pulled the model (tens of GB). When the generation-server chip in
the header changes from 모델 올리는 중 to the model name, it's ready. First boot takes a few minutes.
Shut the GPU down when you're not using it. That is where the hourly bill comes from.
ASG=$(aws cloudformation describe-stacks --stack-name StoryboardDemo \
--query "Stacks[0].Outputs[?OutputKey=='GpuAsg'].OutputValue" --output text)
aws autoscaling set-desired-capacity --auto-scaling-group-name "$ASG" --desired-capacity 0 # off
aws autoscaling set-desired-capacity --auto-scaling-group-name "$ASG" --desired-capacity 1 # onTear everything down: npx cdk destroy
Open the site in two windows and log in as different people — a new tab starts a fresh session, so
u2 in one window and u3 in the other. Both are looking at the same board.
Example: prompt → story → art → review → approval
- Plan (
u1). Press + 이야기 기획 in the sidebar. Type what the film is about, pick genre, tone, runtime, cut count and how many characters to invent, then press 개요 만들기 (~10 s). Read the outline — title, logline, synopsis, the characters it invented, the beat list — and press 이대로 컷 만들기. Characters, their pose sheets and the cuts all land on the board at once. Assign a cut tou2. With a story already on the board, the same dialog offers 다음 회차 and 스핀오프: those land as a new episode in the sidebar and leave the existing cuts alone. (Already have a script? 시나리오 → 컷으로 분해 still splits it mechanically, no model involved.) - Character (
u2). Open 인물, add a character, write a short physical description, generate the pose sheet. Mark one pose as the reference — later cuts with that character start from it. - Art (
u2). Open the cut, edit 작업 지시, press 생성. ~15 s. Or upload a hand sketch and press 생성 with 구도 유지 to ink it. Every attempt becomes a version; the version strip compares them. - Review (
u3). Open the drawing, click on it to drop a pin, drag to circle, tag it 조명, and press 수정 요청 with a memo.u2gets a notification and the cut comes back to them. - Fix and approve.
u2regenerates, presses 리뷰 요청,u3presses 승인. The cut locks — nobody can edit approved content until it's reopened. - Ship. Open 타임라인 for per-scene durations, or 인쇄 for a PDF.
While you do this, watch the other window: everything lands there within a second, without a refresh.
| Item | Roughly |
|---|---|
EC2 g6e.2xlarge (L40S 48GB) |
$2.24/hour · ~$54 for a full day (us-east-1 on-demand) |
| EBS 200GB gp3 @ 500 MB/s | ~$27/month (the extra throughput is what makes a model swap ~1 min instead of ~4) |
| ALB | ~$0.025/hour + traffic |
| CloudFront · S3 · Cognito · AppSync · DynamoDB | a few dollars at demo scale |
GPU uptime is essentially the entire bill. Nothing shuts it down for you.
- Pins and circle marks don't appear in print/PDF — they're screen-only.
- The op log has a 30-day TTL. The log is the board, so edits older than 30 days disappear. If you want to keep a board, remove the TTL first.
- Deleting an image version hides it from the board — the log records a tombstone, so v-numbers
never shift and older notes keep pointing at the right drawing. The S3 object stays: the images
bucket has no lifecycle rule and
/img/*is cached for a year. - Generation is one image at a time (one GPU, one server). Simultaneous requests queue.
- One model is resident at a time. Two people who want different models take turns, and each switch costs a load. If that becomes the bottleneck, the fix is a queue plus one worker per model.
- Edits made while your browser was closed are backfilled from the log on reopen. But your own ops that failed to send are retried four times and then dropped silently.
- Story planning is the one call with no retry and no outbox. A 10-second paid call must not be re-sent behind your back, so a failure is shown and you press the button again.
- A landed plan has no undo. Once you press 이대로 컷 만들기, the cuts and characters are ops on everyone's board — delete them one by one. Reading the outline first is the safety net.
- Nothing rate-limits planning. Any planner or director can press the button as often as they like.
- Local mode has no model. The plan dialog still works, but it splits your prompt into a skeleton and says so — the real thing needs the deployed board.
- The first person to open an empty board seeds the starter board from the browser. A real deployment should move that seeding to deploy time.
This is a demo built to show a concept. It is not a product and is not in a state to put into production. Read all of the below before you deploy it yourself. The author takes no responsibility for damages, costs, or leaks resulting from using it as-is. This is not an official AWS product or sample and has no affiliation with AWS.
Cost — the default configuration keeps the GPU instance running. About $2.24/hour, over $1,600 if you leave it up for a month. There is no budget alarm and no auto-shutdown. Deploying starts the meter.
Security — fix these before deploying to an organization
The demo prioritized showing the idea, and the price was leaving the following open. It assumes a demo environment that only trusted people can reach.
- The server does not verify the author of an op. Any logged-in user can record an edit or approval under someone else's name.
- Roles cannot be self-assigned — the app client's write list is the display name only, so
custom:rolechanges take an admin API call. The display name is self-writable, though: a name in the roster is not proof of who someone is. - Role checks for approve, delete (cuts and image versions), and board reset exist only in the UI. The server does not enforce them. (Three exceptions check the token claim server-side: generation on the GPU server, roster role changes in
putOp.js, and story planning inplan.js.) - The five demo accounts share one password and it never expires. Reissue per account.
- The CloudFront → GPU leg is plain HTTP (user → CloudFront is HTTPS). Attach a domain and terminate TLS, or use a VPC origin.
- AppSync logging, CloudFront access logs, and DynamoDB point-in-time recovery are all off. There is no record to trace an incident with.
- The generation server runs as root and there is no upper bound on the step count in a request. A logged-in user can hold the GPU for a long time.
- Deploying with
SB_DEMO_PWset writes the password into a public file (aws-config.js). Never set it for a public demo. - Story planning has no rate limit and no spend cap. The resolver checks the role and clips the token count per call, but a planner or director can call Bedrock in a loop. Add a per-user quota, or an AWS Budgets alarm, before letting a wider group in.
Image generation — art is produced by open-weight models (Chroma1-Flash, FLUX.2 klein 4B, Chroma1-HD) running on our own EC2 instance. You are responsible for checking the license of each model and its weights and what you may do with the output. Korean prompts are translated to English with Amazon Translate — prompts leave for AWS Translate. Output may not match the prompt, and the same seed produces different images when the model or library versions change.
Data — board contents, uploaded sketches, and generated images are stored in this account's S3 and
DynamoDB. Encryption is S3 default (AES256) only, with no separate KMS key. Do not put real
production material or personal data in it. cdk destroy deletes the buckets and the table with it —
that is irreversible.
Characters — the starter board's script, characters, and names (Yeorum Studio, Kim Hana, Lee Dohyun, Park Seojun, Choi Yujin) are fictional and exist only for the demo.
See CONTRIBUTING for more information.
This library is licensed under the MIT-0 License. See the LICENSE file.