Engineering @ Luvu · Field notes

I just wanted my avatar to wear shoes.

The real shoe-fitting saga: failed overlays, SAM 3, local Qwen, and the cloud-to-Mac pipeline that finally made a tiny pair of sneakers look worn.

An implementation snapshot of Luvu’s beta and latest development work. Availability can differ by app build. The article includes current capabilities, tradeoffs and remaining limits.

In this article
  1. One small feature
  2. Shoes are not stickers
  3. SAM finds the shoes
  4. The layering trap
  5. The eureka moment
  6. Bound the edit
  7. Manifest & renderer
  8. Cloud to Mac
  9. Make it reliable
  10. Choose. Try. Keep.
01 / A perfectly reasonable request

Put these shoes on that little guy.

I wanted my cartoon avatar to wear a pair of blue sneakers. That was the feature.

Luvu is the little iPhone world I’m building for my wife and me. We already had AI-generated avatars, expressions, hair recoloring, and a separate Asset Studio where I could create accessories with a local Qwen image model and publish them to a cloud catalog. Custom glasses worked. Earrings worked. A birthday message on my glasses worked.

So, naturally, shoes should be easy.

Dear lord.

The personOriginal Luvu avatar in a green outfit with small brown shoes.

My existing avatar. Keep the character.

The designStudio-generated blue, black and white high-top sneaker with a prominent tongue label.

The Studio sneaker. Make it look worn.

The assignment. These are the actual experiment inputs, not illustrations made for this article. The images throughout are retained outputs from the failed attempts, the successful local proof, and the later native integration.

What followed involved segmentation, image synthesis, alpha compositing, expression registration, immutable asset manifests, Google Cloud, Cloudflare, and an M4 Max MacBook Pro doing inference at home. All because a tiny person needed better footwear.

The useful part of this story is where each seemingly sensible solution stopped working—and why the eventual answer required giving three different components three very different jobs.

02 / The first wrong assumption

A shoe has a relationship with a leg.

A shirt sticker can often be an overlay: position it, scale it, composite it over the shirt. Shoes are less cooperative. The original avatar is a finished image. Its brown shoes are already painted into the same artwork as the trousers, hands, face, and hair. There is no convenient “remove footwear” layer waiting underneath.

Replacing them means answering several questions at once. Which pixels belong to the old shoe? Where does the trouser cuff end? Which parts of the new shoe sit behind the ankle, and which sit in front? What perspective should each foot have? Where is the ground?

A transparent PNG contains none of those relationships. A bounding box gives you a rectangle. The person wearing the shoe is unfortunately not a rectangle.

Attempt one: let the user erase the old shoes.

We built a manual removal flow. It asked the person to mark the footwear before applying its replacement. In theory, this offered control. In practice, I was trying to dress my avatar and had been promoted to junior roto artist. Even the undo behavior was frustrating.

I rejected that flow. A feature can be technically possible and still be a terrible thing to ask a person to do. “Draw a careful mask around your ankles” was not the Luvu experience I had in mind.

The product constraint

Choose a shoe. See it on your avatar. Decide whether you like it. The implementation had to absorb the complexity that the interface was handing to the user.

03 / SAM 3 enters the chat

We found the shoes. We had not fitted them.

We tried Meta’s SAM 3 locally. Its text-prompted segmentation let us ask for shoes and get masks for the existing footwear. A separate trousers pass supplied the neighboring fabric region. No brush strokes, manually placed points, or hard-coded coordinates for my particular avatar.

SAM 3 highlights the avatar’s two original shoes in pink and green while leaving the trousers and upper body unselected.
Text prompt: shoes

Two masks.
No paintbrush.

The colored regions are detected footwear. This solves selection. It does not tell us how a different sneaker should wrap around these ankles.

A real win, with a boundary. SAM selected the original shoes automatically. The colors visualize its masks; they are not part of the avatar.

The initial exploration used an MLX conversion; after access was approved, we also ran the official weights with PyTorch on Apple’s MPS backend. The official-weight test found both shoes on all six neutral avatars in that small sample, and the subsequent replacement experiment covered 25 expression images across three avatars. That was encouraging evidence for selection—not a benchmark proving universal accuracy.

Now we could remove the old footwear automatically, fit the catalog PNG into the detected foot regions, and let the original trousers sit in front.

It worked. Sort of.

04 / Two wrong answers to “which goes on top?”

First we hid the shoes. Then we hid the legs.

With the trousers in front, the upper part of the sneaker disappeared. Including the tongue label—the very detail I wanted to show. I had generated a recognizable high-top and somehow shipped a blue toe.

So we moved the shoe in front of the trousers.

The label was now extremely visible. The entire shoe was extremely visible. It looked like someone had attached two sneakers to the front of my shins.

Attempt A · behind the trousersNative avatar render with the high-tops hidden behind the trouser cuffs; only the blue toes remain visible.

Natural coverage, missing the design.

Attempt B · in front of the legsFailed native render with oversized tall sneakers layered over the front of both trouser legs.

Visible design, deeply questionable anatomy.

The actual failed renders. Both used automatic footwear selection. Changing the layer order fixed one symptom and created another. Neither produced a convincing ankle-to-shoe connection.

My feedback at this point was: “It doesn’t look the person is actually wearing it.” That was the bug. It wasn’t a missing slider or a slightly incorrect offset.

A worn high-top has mixed occlusion: the rear collar can be behind the ankle, the tongue can sit in front of the cuff, and fabric can overlap the sides. A single image placed entirely above or entirely below another image cannot express all of that. Mirroring one shoe for the other foot also mirrors its lettering and asymmetry; it does not invent the appropriate opposing perspective.

We were asking a compositor to infer missing geometry and material relationships. It was doing exactly what we told it. That was the problem.

05 / The moment it finally looked worn

Give Qwen the character and the shoe.

The breakthrough was to use the Studio image as a design reference for a fitted rendition. SAM would tell us where the intervention belonged. Qwen would reconstruct how the shoe should look on this particular character.

We sent the local Qwen Image 2.1 runtime two visual references: a lower-body crop of the original avatar and the actual Studio sneaker. The instructions described the relationship we needed: matching left and right shoes, proportionate feet, soles at the existing ground height, a rear collar behind the ankles, a short visible tongue, believable cuff overlap, and subtle contact shading.

“Do not extend the shoe up to the knees” made it into the prompt. Experience is a wonderful teacher.

Before · pasted onto the legEnlarged failed attempt: tall shoe tongues cover much of the trouser legs with no convincing opening around the ankles.
After · the leg enters the shoeEnlarged successful Qwen result: proportionate sneakers with opposing foot angles, visible short tongues and cuffs meeting the shoe collars.
The ankle test. Enlarged crops from the retained outputs. The successful result changes scale, perspective, and the cuff-to-collar join together. These are small source images enlarged for inspection; no additional AI enhancement was used for the article.

The first successful local run took about 217 seconds on my M4 Max. It used a 768-pixel square generation canvas, 25 Euler/simple steps, CFG 1, and a recorded seed. This was actual local inference through the pinned ComfyUI Qwen Image 2.1 package, not a manually retouched mockup.

The shoes finally looked like something the little guy had put on.

“BRO THOSE IMAGES LOOK PERFECT! lol”

That was my actual reaction to the demo. A completely reasonable response after spending this much time thinking about cartoon ankles.

Original artworkOriginal full avatar wearing its brown shoes.
The first successful local fitThe same full avatar wearing properly proportioned blue sneakers, with the face, hair, hands and upper outfit preserved.
Check the whole character, too. The shoes should improve the avatar without turning the rest of it into a slightly different person. This was the original single-expression proof; the app integration came next.
06 / The model proposes; the compositor enforces

“Please preserve the face” is not a guarantee.

We do not trust a prompt to preserve unrelated pixels. The generated crop is candidate material, not permission to replace the whole avatar.

SAM’s shoe bounds determine the lower-body context crop. We expand the footwear region enough to allow a new silhouette and include a short section of the detected trouser cuffs. The final edit boundary is softened at its edges. The crop provides context; the mask determines which output pixels may actually change.

Three jobs · three distinct responsibilities

1. SAM 3 locatesOriginal avatar → shoe and trouser masks → context crop and edit boundary
2. Qwen proposesAvatar crop + catalog shoe → a newly synthesized worn pair
3. Code constrainsOriginal + generated patch + bounded mask → reviewable candidate

The segmentation mask constrains final composition. It is not passed into this workflow as a diffusion inpainting mask.

The central design. Segmentation, synthesis, and preservation are separate responsibilities. A better prompt helps the middle stage; it cannot replace the last one.
Original avatar with a rose highlight confined to the shoes, nearby silhouette and lower trouser cuffs.
First proof · measured result
6,248pixels changed inside the allowed area
0protected pixels changed

Both source files remained unchanged. The rose area is the permitted boundary, not the set of pixels that necessarily changed.

Preservation audit. These numbers describe this one retained experiment, not every future fit. The original avatar and Studio asset remained immutable.

The integrated service returns an unblended patch plus a mask. The app validates source and accessory hashes, pipeline version, image dimensions, byte budgets, and mask bounds. The current policy rejects masks that touch the upper 55% of the canvas or cover more than 30% of it. Those are explicit guardrails for the supported standing-avatar layout, not an anatomical model for arbitrary poses.

The compositor blends the permitted region once, using premultiplied color and alpha. At zero mask weight it retains the original pixels. Within the region, transparent replacement pixels can remove old footwear. An ordinary “draw the new PNG over the old PNG” operation would leave the old shoe visible wherever the new one is transparent.

// Conceptual: all four channels are premultiplied RGBA.
if maskWeight == 0 {
    output = original
} else {
    output = original * (1 - maskWeight)
           + generatedPatch * maskWeight
}

Returning a raw patch also avoids applying the same soft mask twice, which can leave halos or remnants around the silhouette. Visual review on light and dark backgrounds still matters: hash correctness does not establish that a shoe looks good.

And Qwen is synthesizing a rendition. It can reinterpret logos, lettering, and design details. A second SAM pass finding two shoes is a useful sanity check, not a fashion critic. The person still gets the final decision.

07 / Keep the design reusable and the fit personal

One catalog shoe. A fit for this avatar.

Publishing an accessory and fitting it are different operations. Studio creates the reusable design. The avatar editor creates a private derivative for a particular character. Mixing these would put personal artwork into a public catalog or force everyone to wear the same badly aligned pixels.

The pipeline from creator to wearer

Studio · create → revise → review → PublishLocal Qwen produces the design. Studio prepares artwork and LuvuAssetManifest, validates the package, uploads versioned objects, verifies them, then updates the catalog.
Public catalog · reusable designAsset ID · version · manifest hash · image hash · shoe category and attachment intent

The phone downloads and verifies the selected package ↓

Private fitting · original avatar + selected designSAM + Qwen + bounded composition → expression preparation → review → Keep this pair → Save
Saved derivative · ordinary native renderingAccepted frames + edit recipe → AvatarAssetManifest → AvatarView. Reopening does not run inference.
Two publication boundaries. The generic design becomes public only through Studio’s Publish action. Personal fitting inputs and results use private storage. These article images are deliberately selected examples, not a public browsing route into that storage.

The public LuvuAssetManifest remains the design contract. Existing published shoe designs can select the new fitting path without being republished. Arbitrary text in a downloaded manifest cannot expand the fitting service’s fixed edit policy.

Locally, OriginalAvatarArtworkEdit version 5 records a fittedFootwear reference and verified fittedFrames. The reference pins the original neutral image hash, asset ID and version, manifest hash, design image hash, job ID, and pipeline version. An old result for a different avatar or revised design must not silently become the current fit.

The render order stays deliberate: immutable original artwork, accepted fitted base, hair recoloring, then ordinary overlay accessories. Changing hair color should not remove the shoes. Reopening a saved look should not contact a model. Removing the fit should recover the original footwear.

For expressions, the app’s existing image-registration machinery transfers the footwear patch and mask into each existing expression frame. It preserves the original face instead of asking Qwen to recreate eight versions of the same person. Saved output still has the normal AvatarAssetManifest expression entries, so the renderer consumes ordinary artwork.

NeutralNative prepared neutral avatar frame with fitted blue shoes.
ExcitedThe existing excited expression with an open smile and the fitted blue shoes preserved.
TiredThe existing tired expression with lowered eyelids and fitted blue shoes.
From demo to app. Real prepared frames from the native integration. This preserves existing expressions; it does not create a walking rig, independent foot motion, or new animation frames.
08 / Somehow my laptop is involved

An iPhone, Google Cloud, Cloudflare, and my Mac.

The good-looking demo ran locally. The next question was obvious: how does selecting a shoe on an actual iPhone reach that pipeline?

We kept account ownership and durable coordination in Luvu’s existing Google Cloud backend. The expensive model work runs on my M4 Max with 48 GB of unified memory, reached through a dedicated Cloudflare Tunnel. The phone talks to the normal authenticated API; it does not know my laptop’s address or receive a machine credential.

Request and result path

iPhone · avatar editorExisting Luvu session · submit selected inputs · poll status · review result

HTTPS / account authentication ↓ ↑ Private result

Google Cloud
Cloud Run APIAuthorize owner, validate catalog, reserve job
Firestore + private StorageDurable job record, input objects, verified result

Cloud Tasks → IAM-protected Cloud Run worker

Worker · stable job ID, short requests, durable retriesSubmit / poll the Mac. Commit the result to private cloud storage before withdrawing its temporary Mac copy.

Short-lived, request-bound machine JWT ↓ ↑ Patch + mask

Cloudflare Tunnel · transport to the local fitting originThe Mac connector establishes an outbound connection. Application authentication still gates each request.
M4 Max Mac · private local runtime
Loopback fitting service → SAM 3 → Qwen → validationOnly fitting routes are exposed through the tunnel. Studio and the raw ComfyUI server stay local.
The deployed route. Cloudflare carries the connection; Luvu’s account service and machine authentication decide who can use it. The model runtimes are not bundled with the iPhone app.

Cloudflare Tunnel supplies outbound connectivity from the Mac without opening an inbound router port. It does not magically authorize fitting requests. We add short-lived machine JWTs bound to the HTTP method, path, and request-body hash. The secret lives server-side and in an owner-only local file—not in the app, Studio browser code, or catalog.

Cloud Tasks and the private Cloud Run worker keep the operation independent of one phone connection. Firestore records the owner and job state. Private Cloud Storage holds inputs and results. The backend independently checks the published design rather than trusting a client to declare any uploaded image “the catalog shoe.”

On the Mac, SAM runs as a separate process that exits before the Qwen fitting stage, releasing its model allocations. Qwen uses the separately installed ComfyUI runtime. LaunchAgents keep the fitting origin and tunnel available; Qwen must also be running. Nothing about installing the app installs Python, SAM, or Qwen on the phone.

This avoids renting a continuously running cloud GPU for our current beta. It does not make inference instant or infrastructure free. The honest availability diagram includes a laptop that must be awake, online, and ready. If I close the lid and leave, I have effectively taken the tiny shoe workshop on vacation.

09 / The demo is not the job system

Retrying a request should not manufacture another pair.

A beautiful output is one milestone. Surviving a lost connection halfway through producing it is another.

A fit has a content-based identity derived from the source image, design bytes, catalog manifest and version, and fitting pipeline version. Identical concurrent submissions for the same account reuse work. The cloud worker submits a stable job ID to the Mac, so a lost response does not become an excuse to start another inference.

queued → segmenting → fitting → checking → ready
                     ↘ failed / cancelled

ready → user reviews → keep or discard
                     → save accepted frames locally

“Ready” means a candidate is available. It does not mean the person has accepted it. Failed or discarded candidates do not become their avatar.

Cancellation and deletion leave tombstones so a delayed task cannot recreate a withdrawn job. Account deletion blocks new reservations and schedules removal of private copies, including cleanup on the Mac after it reconnects. Inputs are removed after completion; cloud results have a seven-day retention window. Accepted on-device derivatives follow the local avatar retention policy.

Quotas and concurrency limits bound the work. If the Mac is unavailable, new fitting is unavailable too; saved avatars still render. This is a durable cloud gateway to a single inference machine, not a highly available GPU fleet. Keeping that distinction visible is much healthier than calling a tunnel “autoscaling.”

What we checkedWhy it matters
Wrong source, design, or result hash; invalid dimensions; escaped edit boundaryA valid response must belong to this exact fit and stay within its permitted region.
Duplicate dispatch, lost responses, cancellation, account deletion racesNetwork uncertainty must not create extra inference or resurrect withdrawn artwork.
Review, discard, retry, accept, recolor, save, relaunch, removeThe result must work through the editor’s real lifecycle, not just in a comparison image.
Existing expressions and unchanged original filesNew shoes must not cost the avatar its face, feelings, or recoverable original.
Release bundle inspectionStudio, model runtimes, and development credentials stay outside the iOS application.

The cloud integration test went from an iPhone SE simulator through Google Cloud, Cloudflare, and the Mac, then through review, cached retry, saving a hair recolor, and reopening. That complete test took 358 seconds. The 217-second figure earlier was one generation; these measure different scopes, and neither is a latency promise.

Ordinary automated tests use fixtures or mocks. Real model inference is an explicit integration check. Otherwise every test run becomes a surprisingly elaborate way to heat a MacBook.

10 / What the person actually gets

Choose. Try. Keep.

The creator designs a shoe in Studio, revises it, reviews the package, and presses Publish. The wearer opens the app’s collection, chooses it, waits for fitting, checks the preview, and keeps or discards the result. Once saved, the avatar opens normally—even when the inference machine is unavailable.

There is no ankle-erasing homework.

We are not claiming arbitrary virtual try-on is solved. Unusual poses, long skirts, boots, extreme proportions, and exact logo reproduction need more evaluation. The current fitting pipeline supports shoes. Watches, hairstyles, and other replacements may benefit from the same separation of responsibilities, but each needs its own boundaries and evidence. Independent leg animation is another problem we deliberately postponed.

The lesson I’m taking forward is practical: use segmentation to find the intervention, generation to propose the missing visual relationship, deterministic code to restrict changes, and the person’s preview to decide whether the result is worth keeping.

Every failed attempt helped expose which responsibility we had assigned to the wrong component. The brush put too much work on the person. Layer order put too much intelligence into a PNG. A prompt alone would put too much trust in the model.

The shoes were small.
The lesson wasn’t.

The final interface asks the person one useful question: do you like how these look on you? Everything else is our problem.

And yes, the tiny guy finally has his sneakers.

Build notes & further reading

Written from the retained experiments and implementation at revision aeed8d7, September 22, 2026. The selected avatar images are mine and are published here to document the work. Existing experiment outputs were reused; no new image generation was performed for this article. Code paths below identify implementation areas, not public downloads of private fixtures.

AreaImplementation record
Segmentation and failed replacement studiestools/sam3-poc/RESULTS.md
First successful Qwen fitting and preservation audittools/sam3-poc/FITTING_DEMO.md
Integrated local inference pipelinetools/avatar-fitting/
Ownership, catalog checks, cloud jobs, and machine authenticationserver/src/fitting/
Native composition, edit persistence, and expression transferSources/LuvuCore/AvatarCreator/OriginalAvatarFitting.swift
OriginalAvatarArtworkEdit.swift
OriginalAvatarExpressionRegistration.swift
Studio publishing and asset contracttools/asset-studio/
shared/avatar-schema/

For the surrounding app, renderer, publishing, account, and relationship systems, read The architecture behind a little world for two. The model and infrastructure links in this article point to their respective maintainers’ documentation.