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.

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.
Mobile-first flow for guest checkout, paid upload, async status, and return-to-gallery email delivery.
Durable order state, photo storage, payment transitions, queue handoff, and result persistence.
Validated image inputs became personalized checkpoints, style merges, prompt batches, and final portrait sets.
User selects package, themes, and subject traits needed by prompts.
Stripe checkout creates a durable paid order before expensive GPU work starts.
Browser validates faces and crops images to the model's expected 512x512 input.
Backend queues a training job and shows progress as an order, not a blocking request.
Gallery supports public/private viewing, trial unlocks, downloads, and bonus content.
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.
Pages covered landing, subject info, themes, payment success, upload, dashboard, and gallery.
Local state carried the funnel; query invalidation refreshed dashboard status after upload.
Amplitude, GA, pixels, email links, and live chat made the product measurable and supportable.
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.
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.
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.
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.
Users could pay with an email and receive a magic upload path even without an active session.
Backend guards prevented unpaid uploads and duplicate training transitions.
The result save endpoint exits if the order was already fulfilled, limiting duplicate queue effects.
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.
Order status changes to training before the GPU worker starts.
Max records is one because each job can monopolize a GPU for a long time.
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.
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.
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.
Adjusted optimizer, epochs, regularization, precision, and checkpoint behavior around real customer photo sets.
Version pinning and binary compatibility consumed more time than expected.
The web app was straightforward compared with getting identity, style, prompt quality, and GPU reliability right.
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.
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.
Long-running workers cannot let checkpoints and images pile up per order.
Duplicate result-save messages do not create a second fulfillment pass.
After cleanup, reproducing an order requires new customer inputs or retained model artifacts.
Users expected to leave and come back later, which made one-GPU, hour-plus jobs viable.
Simple scheduling matches a system where the bottleneck is expensive, stateful GPU time.
Web and GPU services exchanged S3 keys and job schemas instead of streaming image files through APIs.
Separate paid jobs, trials, retries, and manual reruns into priorities with clear concurrency limits.
Persist each pipeline transition so support can answer where an order is stuck.
Automated scoring or human review queues would matter once result volume increases.
Running my own GPU meant I was not just reselling a commodity API. I could tune quality and spend real compute per order.
That same edge made the product expensive to operate. It needed paid conversion, not just usage.
The product was better than competitors at the time, but marketing was weak and there was no built-in distribution loop.
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.
Owning the GPU and model path created room to compete on output quality.
One-hour-plus delivery made the payoff less immediate than consumer AI users increasingly expected.
Poor marketing and no durable acquisition loop meant quality did not translate into scale.
Checkout, upload guidance, async expectations, email delivery, gallery, support, and trial upsell.
Postgres tracked order state. S3 stored photos. Kafka bridged the web app and the single GPU worker.
Personalized checkpoints were merged with style models and used to generate themed galleries.
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.
The wait was not hidden; the product flow made long-running generation normal.
GPU scarcity made queueing, status, and support more important than realtime UX.
Payment, upload, and fulfillment emails brought users back to finish uploads or view completed galleries.