Back to portfolio
detail
00 2023 · Product constraint

AnimePics let users upload photos and see themselves as anime.

Built in 2023, before later image models made this interaction feel instant and trivial. AnimePics was a consumer funnel that collected subject info, payment, and training photos, then used GPU-backed ML to create personalized portrait galleries.

Risk
Non-deterministic ML
input quality drives output quality
GPU work is slow and expensive
users leave before async value arrives
->
needed product and systems design
Shape
Order pipeline
fast mobile purchase flow
validated, cropped training images
async GPU fulfillment and gallery delivery
AnimePics product screenshot
product screenshot · upload photos, get generated anime portraits
Grounding · system shape
The work was a whole product system, not just a diffusion script.

The system split into a Next.js frontend, a Python API/worker backend, a GPU diffusion server, and a separate image workspace for style experiments and regularization assets. The frontend code shows the user path directly: subject info, theme/package selection, Stripe checkout, upload, dashboard/order completion, and gallery delivery.

product surface
Funnel and delivery
checkout, upload, email, gallery

Mobile-first flow for guest checkout, paid upload, async status, and return-to-gallery email delivery.

service layer
Order system
Postgres, S3, Stripe, Kafka

Durable order state, photo storage, payment transitions, queue handoff, and result persistence.

generation layer
GPU pipeline
custom training and inference

Validated image inputs became personalized checkpoints, style merges, prompt batches, and final portrait sets.

detail
01 Funnel

The flow had to feel simple before the long wait.

01
Choose

User selects package, themes, and subject traits needed by prompts.

02
Pay

Stripe checkout creates a durable paid order before expensive GPU work starts.

03
Upload

Browser validates faces and crops images to the model's expected 512x512 input.

04
Wait

Backend queues a training job and shows progress as an order, not a blocking request.

05
Share

Gallery supports public/private viewing, trial unlocks, downloads, and bonus content.

Product decision Keep the customer-facing steps familiar: choose, pay, upload, leave, return by email. The expensive GPU work runs after the product has the inputs it needs.
Grounding · frontend implementation
The UX gathered model parameters without exposing the user to model language.

The Next.js app stored funnel state with Zustand, authenticated with NextAuth, used Stripe checkout, and passed subject traits such as hair color, eye color, glasses, sex, and age into the backend order. The upload page then created a multipart payload of training photos under the `training_photos` field.

frontend
Next.js + Tailwind
mobile-first flow

Pages covered landing, subject info, themes, payment success, upload, dashboard, and gallery.

state
Zustand + React Query
flow state and cache

Local state carried the funnel; query invalidation refreshed dashboard status after upload.

growth loop
Email, analytics, chat
operating a consumer app

Amplitude, GA, pixels, email links, and live chat made the product measurable and supportable.

AnimePics transformation product graphic AnimePics generated result AnimePics generated result AnimePics generated result
detail
02 Input quality

Better input photos made the customer result better.

Picture quality had an outsized effect on whether customers liked the final gallery. I added ML face detection and smart cropping in the browser so users could fix bad inputs before the async training process started.

  • Minimum input size. Images below 512 by 512 were rejected.
  • Face count checks. No-face and likely multi-face photos were rejected.
  • Centered crop. Smartcrop used the detected face as a boost region and emitted 512 by 512 files.
client-side gate
Phone photos->face-api.js->smartcrop->S3-ready files
no facetoo smallface too largelikely multiple people
Grounding · quality tradeoff
Client validation improved satisfaction, but moved compute into the upload UX.

The browser loaded face detection models and warned that validation could take several seconds per uploaded photo. That was an intentional product tradeoff: a slightly slower upload flow was better than letting customers wait for a gallery trained on faces that were too small, missing, or mixed with other people.

Why this mattered The ML pipeline used up to 20 training photos. Better crops and cleaner face selection gave the personalization step a much stronger signal, which directly affected whether the final portraits looked like the customer.
detail
03 Web backend

The backend tracked orders, stored photos, and handed off GPU jobs.

The Python web service tracked payment state, user sessions, order metadata, photo metadata, and queue handoff. S3 stored the image files. Kafka connected the web server to the GPU worker without keeping HTTP requests open.

  • Postgres: users, payors, orders, themes, training photos, result photos.
  • S3: training and result image objects organized by order ID.
  • Redis: session and cache state updated after payment and upload transitions.
web service boundary
Next.js->
Litestar APIPostgres
->
S3 photosKafka job
Grounding · payment consistency
Expensive work starts after the order enters a paid or admin-approved state.

Order creation upserts a started order and creates a Stripe checkout session. The Stripe webhook marks the order paid, stores payment intent information, updates the user's `order_in_progress`, and emails guest users an upload link. Upload refuses started, already training, or already trained orders.

funnel
guest checkout
lower conversion friction

Users could pay with an email and receive a magic upload path even without an active session.

integrity
status machine
started, paid, training, trained

Backend guards prevented unpaid uploads and duplicate training transitions.

idempotency
fulfilled_at check
result save guard

The result save endpoint exits if the order was already fulfilled, limiting duplicate queue effects.

detail
04 Async jobs

A single queue message represented a long GPU transaction.

The web server published one training job message with the order ID and subject traits. The GPU consumer pulled one job at a time, allowed a six-hour poll interval, retried failures, and manually committed offsets after the handler completed.

  • Why Kafka: durable async handoff between web traffic and scarce GPU workers.
  • Why one record: one personalized model job is naturally large and stateful.
  • Risk: partial progress inside the job is not broker-visible.
job handoff

Web worker

upload complete->training job

Order status changes to training before the GPU worker starts.

GPU worker

consume one->commit after success

Max records is one because each job can monopolize a GPU for a long time.

Grounding · orchestration tradeoff
The pipeline was one queue job, with restartable artifacts between stages.

Inside one handler, the GPU server downloaded training photos, created models, ran inference, uploaded results, cleaned local files, and then produced a result-save job back to the web service. I kept stage outputs on disk while the job was active, so a failure during inference or upload did not automatically mean starting from raw photos again.

Scaling implication For one operator and scarce GPUs, local stage artifacts were enough to restart failed work manually. At higher volume, the natural evolution is to make those stage checkpoints explicit in the backend: downloaded, model trained, inference complete, uploaded, persisted, notified.
detail
05 Model pipeline

Training created a person model, then merged it into style systems.

The GPU server trained a personalized checkpoint, then merged that identity model with themed secondary checkpoints such as cartoon, ghibli, shinkai, and pixar before running prompt batches. The result was not a single prompt call; it was a multi-stage personalization and generation pipeline.

  • Base model: personal identity captured from user photos.
  • Style systems: theme checkpoints blended with the personalized model.
  • Prompt batches: sex, age, hair, eyes, and glasses filled prompt templates.
model assembly
AnimePics generated portrait in American illustration style AnimePics generated portrait in cartoon style AnimePics generated portrait in anime style AnimePics generated portrait in cinematic anime style
20 crops->custom training->identity checkpoint->themed gallery
Grounding · ML engineering reality
The hard part was turning research tooling into a repeatable product pipeline.

The training service customized more than the base algorithm: dynamic epoch count based on uploaded image count, sex-specific regularization sets, tuned optimizer and scheduler parameters, checkpoint merging, model switching, prompt templates, deterministic seed lists, high-resolution fix, and upscaling. It also restarted the image-generation service after training to work around memory pressure before inference.

customized
Training parameters
identity quality

Adjusted optimizer, epochs, regularization, precision, and checkpoint behavior around real customer photo sets.

failed pain
GPU package management
environment fragility

Version pinning and binary compatibility consumed more time than expected.

lesson
ML took longer
product quality lived here

The web app was straightforward compared with getting identity, style, prompt quality, and GPU reliability right.

detail
06 Reliability

The system had to recover without keeping everything forever.

Media files were the operational center of gravity. The web service stored durable S3 keys and DB rows. The GPU worker used local disk for transient training photos, model checkpoints, result images, and training artifacts, then cleaned them after upload.

  • Durable: order state, S3 training photos, S3 result photos, result metadata.
  • Temporary: local GPU files and generated checkpoints after fulfillment.
  • Cost control: scheduled cleanup deletes old training photos after fulfillment.
storage lifecycle
Grounding · reliability choices
The cleanup model reduced cost, but narrowed the replay window.

The backend has a cron path for deleting training photos once orders are fulfilled and older than a threshold. The GPU worker also deletes local per-order photos, result images, model files, and training directories after upload. This is the right instinct for a cost-sensitive consumer product, but it trades easy future reprocessing for lower storage and disk pressure.

good
bounded disk use
GPU worker cleanup

Long-running workers cannot let checkpoints and images pile up per order.

good
result save guard
fulfilled_at

Duplicate result-save messages do not create a second fulfillment pass.

tradeoff
limited replay
cost versus recovery

After cleanup, reproducing an order requires new customer inputs or retained model artifacts.

detail
07 Scaling path

The first architecture was right for scarce GPUs, not unbounded demand.

Product

Async promise,
not realtime

Users expected to leave and come back later, which made one-GPU, hour-plus jobs viable.

Compute

One job per
GPU worker

Simple scheduling matches a system where the bottleneck is expensive, stateful GPU time.

Data

Object keys between
services

Web and GPU services exchanged S3 keys and job schemas instead of streaming image files through APIs.

Next scaling step Add durable per-stage state, GPU capacity-aware scheduling, dead-letter queues, artifact retention policy by order status, and user-visible ETA based on queue depth and historical job time.
Grounding · what I would change at higher scale
The system needs more state before it needs more clever ML.
scheduler
capacity-aware queueing
GPU pool pressure

Separate paid jobs, trials, retries, and manual reruns into priorities with clear concurrency limits.

state
stage checkpoints
resume and observability

Persist each pipeline transition so support can answer where an order is stuck.

quality
output ranking
reduce manual curation

Automated scoring or human review queues would matter once result volume increases.

detail
08 Failures

The product was technically differentiated.
The business did not have distribution.

Edge

Owned GPU and
custom ML

Running my own GPU meant I was not just reselling a commodity API. I could tune quality and spend real compute per order.

Constraint

Cost forced
revenue

That same edge made the product expensive to operate. It needed paid conversion, not just usage.

Failure

No GTM
flywheel

The product was better than competitors at the time, but marketing was weak and there was no built-in distribution loop.

Market lesson The idea was directionally right. Later, when the OpenAI image model made "Ghibli-fying" yourself a fad, the mass-market version won on speed, quality, and breadth: roughly seconds instead of an hour, plus support for multiple people, pets, and more arbitrary scenes.
Grounding · why it ended
Quality alone was not enough to overcome speed, cost, and distribution.

AnimePics could produce higher-quality personalized outputs because it controlled the GPU workflow and spent meaningful time per order. But the experience was not magical enough to sell itself: delivery could take hours, the product needed revenue to cover compute, and I did not build a repeatable channel or social sharing flywheel that made acquisition cheaper over time.

technical win
Not commodity API wrapper
custom pipeline

Owning the GPU and model path created room to compete on output quality.

product miss
Not magical enough
delay hurt delight

One-hour-plus delivery made the payoff less immediate than consumer AI users increasingly expected.

business miss
No distribution engine
marketing and GTM

Poor marketing and no durable acquisition loop meant quality did not translate into scale.

detail
Recap product · architecture · learning

Quality-first output.
Async GPU pipeline.
Email-driven delivery.

Ownership

Built the whole
product loop

Checkout, upload guidance, async expectations, email delivery, gallery, support, and trial upsell.

Architecture

Orders in Postgres.
Images in S3.

Postgres tracked order state. S3 stored photos. Kafka bridged the web app and the single GPU worker.

ML system

Custom training to
prompt batches

Personalized checkpoints were merged with style models and used to generate themed galleries.

The through-line AnimePics was a product and systems exercise: set expectations that quality takes time, spend real GPU time per order, and make an asynchronous consumer workflow feel reliable through status, email, and gallery delivery.
Grounding · quality and delivery
The product chose async delivery so the model could spend enough time on quality.

A key product decision was not to promise instant results. With one GPU, a single personalized job could take more than an hour, and a queue of orders could push delivery into multiple hours. The user experience was built around that reality: pay, upload validated photos, leave, then come back from email when the gallery was ready.

expectation
Async by design
quality over immediacy

The wait was not hidden; the product flow made long-running generation normal.

constraint
One GPU
serial expensive work

GPU scarcity made queueing, status, and support more important than realtime UX.

delivery
Email integration
return path

Payment, upload, and fulfillment emails brought users back to finish uploads or view completed galleries.

left/right advance · detail toggle / space flips detail
1/10