AI-powered "magic mirrors" enhance in-store shopping with personalized, interactive experiences.

Peter Bubenik · Shopify ML · · Source
AI-powered "magic mirrors" enhance in-store shopping with personalized, interactive experiences.

Concept 1: What Is a Magic Mirror and Why Does It Exist?

The Problem It Solves

Physical retail is competing with the convenience of online shopping. To survive, stores need to offer experiences that can't be replicated at home.

The Solution

A magic mirror looks like a normal mirror but contains:

  • A hidden display screen behind the glass
  • A hidden camera
  • A computer running software
Normal Mirror View:          What's Actually Inside:
┌─────────────┐              ┌─────────────┐
│             │              │  [Camera]   │
│  Customer   │              │  [Display]  │
│  sees self  │              │  [Computer] │
│             │              │             │
└─────────────┘              └─────────────┘

What It Can Do

CapabilityExample
Recognize appearanceDetect skin tone for makeup matching
Respond with messagesShow personalized compliments
Interact with systemsGenerate discount codes
Recognize gesturesScore a yoga pose

Key Metric

Each interaction takes less than 1 minute, making it scalable for busy stores.


Concept 2: The Hardware Stack (Bill of Materials)

Physical Components Needed

┌─────────────────────────────────────┐
│           MAGIC MIRROR              │
│                                     │
│  [Full-sized mirror frame]          │
│       ↕                             │
│  [Display screen behind glass]      │
│       ↕                             │
│  [Webcam hidden behind display]     │
│       ↕                             │
│  [Computer running the software]    │
│       ↕                             │
│  [Physical button to trigger it]    │
└─────────────────────────────────────┘

Why Each Part Matters

  • Mirror: The physical disguise — it looks ordinary
  • Webcam: The "eyes" — captures the customer's photo
  • Webcam mount: Allows rotation to frame the shot correctly
  • Computer: The "brain" — runs the server and browser
  • Button/keyboard: The trigger — customer initiates the experience

Concept 3: The User Flow (What Happens Step by Step)

The Experience from the Customer's Perspective

Customer walks up
        ↓
Presses button
        ↓
Screen counts down
        ↓
Photo is taken
        ↓
AI analyzes the photo
        ↓
Mirror shows personalized result
(compliment + product recommendation)
        ↓
Result stays visible 10 seconds
(customer can take a selfie)
        ↓
5-second countdown
        ↓
Mirror resets for next customer

The Technical Flow Behind the Scenes

Customer presses button
        ↓
Browser sends HTTP POST to server
        ↓
Server sends photo to AI vision model
        ↓
AI returns JSON with compliment + product ID
        ↓
Server sends result back to browser
        ↓
Browser renders the response screen

Key insight: The entire experience runs on standard HTTP — the same protocol used by every website. No exotic technology required.


Concept 4: The Framework — Why Remix?

What Remix Is

Remix is a full-stack React framework. "Full-stack" means it handles both:

  • Server-side logic (talking to AI, processing data)
  • Client-side UI (what the customer sees on screen)

The Magic of Remix Route Files

In Remix, a single file can contain three things at once:

// ONE FILE does all three jobs:

// 1. SERVER - handles GET requests
export async function loader() {
  // runs on the server
}

// 2. SERVER - handles POST requests  
export async function action() {
  // runs on the server
}

// 3. BROWSER - renders the UI
export default function Component() {
  // runs in the browser
}

Why This Matters for the Mirror

Without Remix:                    With Remix:
┌──────────┐  ┌──────────┐       ┌─────────────────────┐
│ Frontend │  │ Backend  │       │   Single Route File  │
│  (React) │←→│  (API)   │       │  server + UI + logic │
└──────────┘  └──────────┘       └─────────────────────┘
Two separate codebases            One cohesive file

The Key Hook: useActionData

When the server action runs and returns data, the browser component receives it automatically via useActionData() — no manual API calls needed.


Concept 5: Building the AI Prompt

The Three-Part Prompt Structure

The prompt sent to the AI is assembled from three pieces:

┌─────────────────────────────────────────┐
│  PART 1: Base Prompt                    │
│  - Who the AI should act as             │
│  - Tone constraints                     │
│  - What task to perform                 │
│  - What NOT to mention (exclusion list) │
├─────────────────────────────────────────┤
│  PART 2: Product Table (plain text)     │
│  Product ID | Name | When to Recommend  │
│  001        | Rose | Warm skin tones    │
│  002        | Nude | Cool skin tones    │
├─────────────────────────────────────────┤
│  PART 3: Output Schema Instructions     │
│  "Return exactly this JSON shape:"      │
│  { compliment: "", product_id: 0 }      │
└─────────────────────────────────────────┘

Why Plain Text Table (Not JSON) for Products?

FormatProsCons
Plain text tableHuman-readable, LLMs parse it wellLess structured
JSONHighly structuredHarder for brand teams to edit

Design decision: Brand teams need to review and edit the product list. Plain text is easier for non-developers to read and modify.

Prompt Engineering Lessons Learned

LessonBad ApproachGood Approach
Length control"Be concise""No more than 100 characters including spaces"
Compliment quality"Generate a compliment"Provide 50+ pre-approved compliments to choose from
SafetyTrust model defaultsExplicitly list forbidden topics (race, religion, etc.)
InventoryManual overrideAdd inventory field; prompt model to softly prefer low-stock items

Concept 6: The LLM API Call

How the AI Vision Call Works

// Simplified version of what happens:

const response = await openai.chat.completions.create({
  model: "gpt-4-vision",
  
  messages: [{
    role: "user",
    content: [
      { type: "text",    text: builtPrompt },      // the 3-part prompt
      { type: "image_url", image_url: photoDataURL } // the customer's photo
    ]
  }],
  
  response_format: { type: "json_object" }, // force valid JSON output
  max_tokens: 300                           // prevent runaway responses
});

Three Key Decisions Explained

1. response_format: { type: "json_object" }

Without it:  AI might return "Here is the JSON: { ... } Hope that helps!"
With it:     AI returns ONLY valid JSON — safe to parse immediately

2. Image as Data URL

Without:  Upload photo → get CDN URL → pass URL to AI  (3 steps)
With:     Convert photo to base64 string → pass directly (1 step)

3. max_tokens: 300

The prompt limits output to ~2 sentences.
300 tokens is generous for that — but prevents edge cases
where the model generates paragraphs unexpectedly.

Product Matching After the AI Responds

// AI returns: { compliment: "You glow!", recommended_product_id: 3 }

const product = products.find(p => p.id === result.recommended_product_id);

// If product found → show compliment + product card
// If product NOT found → show compliment only (graceful degradation)

Graceful degradation: If something goes wrong, the experience still works — it just shows less information rather than breaking entirely.


Concept 7: Server-Sent Events (SSE)

The Problem SSE Solves

There is only one screen — the mirror itself. But a store associate might need to:

  • Trigger a capture remotely
  • Reset the experience
  • Monitor what's happening

They can't touch the mirror screen while a customer is using it.

What SSE Is

Server-Sent Events = a persistent one-way connection from server to browser

Normal HTTP:                    SSE:
Browser → asks → Server         Server ──────────────→ Browser
Browser ← gets ← Server         (keeps connection open)
Connection closes               Server can push anytime

The SSE Architecture

┌─────────────┐     SSE connection     ┌──────────────────┐
│  Associate's│ ←──────────────────── │   Remix Server   │
│   Device    │                        │                  │
└─────────────┘                        │  SSEClientManager│
                                       │  (in-memory list │
┌─────────────┐     SSE connection     │   of clients)    │
│   Mirror    │ ←──────────────────── │                  │
│   Browser   │                        └──────────────────┘
└─────────────┘

The Channel System

Multiple mirrors can share one server without interfering:

// Each SSE message includes a channel:
{ channel: "booth-1", event: "processing" }
{ channel: "booth-2", event: "complete"   }

// Mirror registered to "booth-1" ignores "booth-2" messages
Server broadcasts to ALL clients
        ↓
booth-1 client: "Is this for me?" → YES → process it
booth-2 client: "Is this for me?" → NO  → ignore it

Concept 8: The Client State Machine

What a State Machine Is

A state machine is a system that can only be in one defined state at a time, with explicit rules for how it moves between states.

The Mirror's Five States

┌─────────┐
│  IDLE   │  ← Mirror shows default screen, waiting
└────┬────┘
     │ button pressed
     ↓
┌─────────┐
│ LOADING │  ← "Step back and smile" + countdown
└────┬────┘
     │ AI returns result (via useActionData)
     ↓
┌──────────┐
│ RESPONSE │  ← Shows compliment + product for 10 seconds
└────┬─────┘
     │ countdown timer ends
     ↓
┌──────┐
│ EXIT │  ← "Please exit the booth"
└──┬───┘
   │ button pressed
   ↓
┌──────────┐
│  RESET   │  ← Page reloads → back to IDLE
└──────────┘

Why a State Machine?

Without it, you'd have messy conditional logic:

// BAD - hard to reason about:
if (isLoading && !hasResult && buttonPressed) { ... }
if (!isLoading && hasResult && !isExiting) { ... }

// GOOD - state machine:
if (currentState === 'LOADING') { ... }
if (currentState === 'RESPONSE') { ... }

The Capture Delay

Button pressed
      ↓
Show "Step back and smile" immediately
      ↓
Wait 2 seconds  ← gives customer time to reposition
      ↓
Take photo

Why this matters: Photos taken at arm's length with the subject centered produce better skin tone analysis than close-up, off-angle shots. This small UX detail materially improved recommendation quality.


Concept 9: The Response Screen and Auto-Reset

The Countdown Timer

After showing the result for 10 seconds, a circular countdown appears:

The circle is drawn with SVG:

Full circle (10s):    ●  (complete arc)
Halfway (5s):         ◑  (half arc)  
Done (0s):            ○  (no arc)

How the SVG Countdown Works

// A circle has a circumference (total length of its edge)
// stroke-dashoffset controls how much of that edge is "erased"

// At start:  dashoffset = 0    → full circle visible
// Each second: dashoffset increases → arc shrinks clockwise
// At end:    dashoffset = circumference → circle invisible
CSS transition: stroke-dashoffset 1s linear

This makes each step animate smoothly rather than jumping.

The Full Response Timeline

0s  ──── Result appears (compliment + product)
         Customer reads / takes selfie

10s ──── Circular countdown begins in corner

15s ──── Screen advances to EXIT state
         "Please exit the booth"

         (next customer presses button)
         ──── Page reloads → IDLE

Concept 10: Screen Transitions

The Problem

Switching instantly between screens looks jarring. A smooth fade feels polished.

The Solution: Fade-Through Transition

State changes from LOADING → RESPONSE

Step 1: Detect state change
Step 2: Fade OUT current screen
        (opacity: 0, scale: 0.98, blur)
Step 3: Wait until fully invisible (midpoint)
Step 4: Swap the actual content
Step 5: Fade IN new screen
        (opacity: 1, scale: 1, no blur)
Timeline:
0ms    ──── Start fade out
250ms  ──── Screen fully invisible → swap content here
500ms  ──── Screen fully visible again (new content)

Key insight: The content swap happens at the midpoint when the screen is fully invisible — so the user never sees a "jump" between old and new content.


Concept 11: Configuration and Reusability

The MirrorConfig Pattern

Each mirror deployment is just a thin config file:

const mirrorConfig: MirrorConfig = {
  brand: "Rare Beauty",
  prompt: "You are a warm, encouraging beauty advisor...",
  products: [
    { id: 1, name: "Soft Pinch Blush - Joy",   when_to_recommend: "warm undertones" },
    { id: 2, name: "Soft Pinch Blush - Hope",  when_to_recommend: "cool undertones" },
  ],
  authSecret: process.env.MIRROR_SECRET
}

What Changes vs. What Stays the Same

┌─────────────────────────────────────────────┐
│  CHANGES per deployment (in config):        │
│  - Brand name                               │
│  - AI prompt / persona                      │
│  - Product list                             │
│  - Auth secret                              │
├─────────────────────────────────────────────┤
│  STAYS THE SAME (reusable infrastructure):  │
│  - React component                          │
│  - Server action logic                      │
│  - SSE system                               │
│  - State machine                            │
│  - Screen transitions                       │
└─────────────────────────────────────────────┘

Result: Once the system is built, spinning up a mirror for a new brand or event takes only a few hours — just write a new config.


Summary: How All Concepts Connect

HARDWARE                    SOFTWARE
─────────                   ────────
Camera ──────────────────→  Captures photo as data URL
Button ──────────────────→  Triggers HTTP POST (Remix action)
Display ←────────────────── Renders React component

SERVER SIDE (Remix action)
──────────────────────────
Receives POST
  → Builds 3-part prompt
  → Calls AI with photo + prompt
  → Gets JSON back (compliment + product_id)
  → Resolves product_id to full product
  → Broadcasts status via SSE
  → Returns result to browser

CLIENT SIDE (React component)
─────────────────────────────
State machine manages 5 states
  → useActionData receives server result
  → ScreenTransition fades between states
  → ResponseScreen runs countdown timer
  → Auto-resets for next customer

REMOTE MONITORING (SSE)
───────────────────────
Associate's device holds SSE connection
  → Receives status updates
  → Can trigger/reset without touching mirror
  → Channel system isolates multiple mirrors

The magic mirror is, at its core, a browser running behind reflective glass — but the thoughtful combination of these concepts creates an experience that feels genuinely magical to the customer standing in front of it.

More to study