Build write-up · 10 min read

How I built Lucy Learns

A dog-training app that works offline, built for my own household. I designed, built and illustrated it alone, with the AI tools run from a terminal instead of a chat window. What that changed about how I work, and what it did not.

Role
Everything: product, design, build
Built with
Plain JavaScript, a service worker and the Web Speech API; no framework, no build step
Runs as
An installable phone app that works offline, served from GitHub Pages
Status
In use, still in development
  1. The Calm Door Greetings track across five stages, with today’s homework and a Start session button.

    01 Pick up where the trainer left off

  2. Mid-session, timer on: step 1 of 6, ring or knock once, in large type, above Pause, Next step in 10s, the rep tally and a count-as-a-miss control.

    02 Run it one instruction at a time

  3. End of session: Ready for the next step, 100% success calm throughout, Lucy trotting across a gold field. Level 1 cleared, 1 of 5 in Doorbell Means Place.

    03 Finish on a win

Three taps of context: the app in the order you meet it.

My approach

  1. 01Research

    • A trainer’s handouts, written for someone sitting still
    • Practice watched where it happens: leash in one hand, five minutes
    • Acceptance criteria written to be falsifiable on a real phone
    The problem
  2. 02Design

    • Progress in the trainer’s levels, not points and streaks
    • One instruction per screen, one tap per rep
    • Voice commands cut after real sessions, then a step timer
    Five decisions
  3. 03Build

    • Plain JavaScript modules: no framework, no build step, no dependencies
    • Service worker for offline, local storage for state, Web Speech API to read the steps aloud
    • Illustrations from JSON specs a Node script posts to an image model
    • AI built working versions in an afternoon. I judged them
    A terminal, not a chat window
  4. 04Test

    • On a real phone: installed, offline, one-handed
    • A contrast script gates every commit against the WCAG floor
    • A blind pilot: four models, shuffled order, filenames hidden
    • Two pictures failed and were redrawn until strangers read them right
    How I checked it

The problem

We hired a professional trainer for our dog and got a stack of handouts: good instructions, written for a person sitting still. Practice happens in a hallway, leash in one hand and treat pouch in the other, five minutes at a time. By evening nobody remembers whether the barking is getting better or it was just a bad Tuesday.

So the app has a narrow job: turn the handouts into something you can run one-handed. It also records enough that “is this working” has an answer. It supports the trainer. It does not replace one or diagnose behavior.

Five decisions that shaped it

Progress is measured in the trainer’s units, not mine

The obvious move is points, streaks or a satisfying progress bar. The unit here is the level, the trainer’s own. The number on the app’s program map is the number in her lesson report, so nobody reconciles the two in their head. Points would have looked more like a product and been worse at the job.

  1. The Activities library, grouped by goal: the Calm Door Greetings track with five stations, and cards for Doorbell Predicts Rewards and Stay While the Door Opens, each with its level.

    The program, in her levels

  2. The Progress tab: Jumping in two sessions this week after none last week, a Summary for your trainer card, six sessions, 83 percent calm, and per-behavior counts.

    The week, ready to hand to the trainer

  3. The Profile tab: Lucy’s card noting she gets over-aroused at arrivals, and Commands we use (“Go to bed,” “Back,” “Stay”) above a line to confirm the wording with the trainer.

    The cues, in her words

All three are hers: her levels, a week to hand over at the next lesson, and the words she says out loud, set once and repeated on every activity screen.

Logging happens during the session, not after it

One tap after each repetition, one more on arousal to save the session. That is the entire logging burden, capped deliberately: anything heavier does not get used with a dog attached to your other arm. The tally records what happened, not a reconstruction at the end of the day.

Speaking and listening, split

Voice looks like one feature. It is two, and they cost nothing like the same thing.

Speakingon by default

  • Runs on the device
  • No permission needed
  • Works with no signal

Listeningoff by default

  • Ships audio to a server
  • Needs a microphone grant
  • Needs a connection
Two switches, not one. Bundled, the reliable half would have been hostage to the fragile one. When the fragile half came out, the reliable one did not move.

Listening came out the first time it met the hallway

The split held up. The listening half did not, and the microphone was not why. It was a misconception, and it was mine: I designed the commands for the handler and forgot who else was listening.

A handler saying “next step” brightly at a phone is saying words in the cue voice. The dog is being taught that words in that voice are for her. The commands avoided every cue and still shared the room with them, and Lucy was distracted by every one. The AI I built it with did not flag that up front either. A couple of real sessions did.

So the listening came out and the steps turn by themselves instead:

  • A switch on the get-ready screen puts each step on a timer. The same control sits on every step, beside a pause and a live count. The pace gets changed where it feels wrong, not on a settings page
  • The Next button fills as the clock runs, so from arm’s length you can see it about to press itself
  • The clock never answers the rep question. It walks the steps and stops on the last one, because a timer that answered it would be the reflex “went well” made automatic
  • One session rewrote every label to say timer or step, because “Move on by itself” was met with “move what on?”
  • The voice reads the step’s number in front of each instruction: the beat the handler wanted, and the cushion an idle audio route needed
  • Reading aloud did not move at all, because it asks nothing of anyone’s voice
1
day to replace it with a timer that walks the steps
0
changes to the speaking half, which shipped on and stayed on

Listening was a conversational interface. It failed on nothing an accuracy figure measures. The words were caught and the steps turned. The number would have looked fine. No review of the design would have found it, mine or the AI’s. The problem was in the room. Only using the product there, with the people and the dog it is for, could show it.

What a voice feature has to settle before it ships is not whether it hears the person using it. It is who else is in earshot, and what that sound has already been taught to mean.

Built for two postures

The session screen is designed around where your hands are, which turns out to be two different places.

A woman in a hallway, phone in one hand, leash in the other, looks down at a black dog with a white chest. A treat pouch hangs at her hip.

In one hand

Phone in whichever hand the leash leaves free. The app reads each step aloud so you never look down, and with the timer on the steps turn by themselves. The one tap left is the one that says how the rep went.

A tablet upright on a wooden stand on a coffee table. Beyond it a woman crouches, both hands open and empty, facing a black dog on a gray floor bed.

On a stand

The setup for the busiest moments: the phone, or an iPad, propped upright in portrait where you practice. The instruction reads from a step away. The steps turn on the timer. Both hands stay with the dog until the rep question needs a thumb.

Same session, two postures. Nothing about the screen changes between them.

A terminal, not a chat window

It did not decide what to build. Every decision above came from watching two people fail to practice consistently, and from twenty years of knowing which shortcuts turn into support tickets later.

The hard part of illustrating a training app is not getting a model to draw a woman training a dog. It is that it has to be the same dog every time, in the same room. Nothing may change from one screen to the next except the behavior being taught.

The dog turns toward her. The leash goes slack. The door opens. That is a design problem before it is a drawing problem. It has an established state, one intended change, and a long list of things that have to stay put.

What the terminal changed is how far I can carry that alone. Each scene is a JSON spec that a Node script assembles and posts to OpenAI’s image model. The reference images go in a set order, each with a named job:

  • The style exemplar, always first
  • A likeness sheet for the handler and one for Lucy: black coat, white chest, scruffy face, collar with the round blue tag, never a harness
  • For an adjacent moment, the previous picture itself, so that nothing but the action may differ
  • The wording to put in front of it, its numbered list generated from that same array
  • One sentence naming what this picture has to get right, which every round is checked against before anyone looks at it twice

A chat window drew the first eight and it worked. What it lost is what makes a bad picture diagnosable: which references were attached, and in what order, the strongest lever on consistency there is.

As ordered data, a wrong request is refused before it costs anything. A redraw that drifts goes back to the last round that was right, not forward from the drifted one. Every round is on file, so going back is a choice rather than a memory.

The four styles the app has had show where the time went.

  1. Painted era: a photographic living room in warm lamplight, the handler reading in an armchair, Lucy asleep on a gray mat, with bookshelves, plants and framed pictures behind them.

    01Painted, in a chat window

    The set the app launched with. It is a picture of a house rather than an instruction. The room has more detail than the step, and at the 56 pixels the program map gives it, none of it survives. Lucy is also not the same dog twice: twelve of the thirty it drew put her in a harness she does not own.

  2. Warm vector era: the same scene flat on a tan wall over a wooden floor, the handler in a wooden-armed chair, Lucy asleep on a gray bed, nothing else.

    02Warm vector, still in a chat window

    The room empties out and the rules about the dog get absolute. Flat fills, one action per frame, never a harness in any image, ever. It was still drawn by pasting paragraphs into a chat window. No round was recorded. No picture could be asked for the same way twice.

  3. Lavender era: the same flat scene again with the wall repainted a pale violet, the chair now upholstered blue, the floor a cooler gray-brown.

    03Cool and flat, from the terminal

    The first set a script drew rather than a conversation: every scene a JSON spec, the reference images ordered, every round on file. It also took the app’s chrome a little too literally and painted the violet onto the walls. That was the one thing nobody asked for.

  4. The shipping era: the same flat scene on a warm plaster wall over a pale wood floor, the blue chair and gray bed unchanged from the previous version.

    04The wall repainted, in one afternoon

    The violet belonged to the app, not the room, and taking it back out meant repainting every wall in the library. The same change a month earlier would have been another five days in a chat window. This time it was an afternoon, with every round on file.

One scene, drawn four ways in a month. A chat window can make a picture. It cannot repaint sixty-three of them between lunch and dinner.
  1. The first-launch welcome: an illustrated handler and Lucy on her bed as a guest waits at the open door, above the heading Five minutes of practice at a time.

    01 First launch, explained with a picture

  2. Get-ready screen, Doorbell Means Place, Level 1: the handler crouched at Lucy’s bed, Lead her to the bed with a treat, a checklist and a Start practice button.

    02 Every level opens on its scene

  3. The Choose your avatar sheet on the profile: fourteen drawn portraits of people of different ages in a grid, the handler’s selected, with Cancel and That is me buttons.

    03 Twenty-four portraits from the same pipeline

Where the pictures sit: the welcome, every level’s opening scene, and the ten dogs and fourteen people a household picks from all come from one script, in one room.

Making a picture got cheap, so making one stopped being the accomplishment. The decisions moved up a level:

  • What has to stay the same
  • What is allowed to vary
  • Whether the picture shows the behavior it claims to
  • When to stop

The judgment did not change. The distance between thinking and making did.

How I checked it before shipping it

Three things, all cheap.

Acceptance criteria that can be falsified

Every line is observably true or false on a real phone, and someone who has never seen the code reaches the same verdict I would. Written that way they are testable rather than admirable.

A blind pilot, before a real panel

The illustrations came out of a model, so the second check is an evaluation. Does the output show what it claims to, judged by someone who was not in the room when it was made?

  • Four language models, no knowledge of the project, saw the same eight images in different orders
  • One question each: what is the person doing, and what is the dog doing
  • Two pictures failed outright, all four viewers misreading them the same way
  • Both were redrawn and passed the same blind check with fresh viewers before shipping

Not a substitute for testing with people. A way to catch the obvious failures for free, before the expensive test is worth running. The shape of it transfers to anything generated:

  • Fix the question the output has to answer before any output exists
  • Put it to viewers with no stake and no context
  • Shuffle the order, so position cannot flatter a picture, and hide the filenames, so the name cannot answer the question on the image’s behalf
  • Read agreement among wrong readings as the strongest signal in the set. Four viewers failing the same way is a fault in the artifact, not four opinions

What makes it an evaluation rather than a reaction is that the pass mark was written down first.

The first version: Lucy with all four feet on the bed, facing the handler’s open hands. Before · 0 of 4
The redraw: most of Lucy off the bed, one back foot still touching its edge, moving toward the open hands. After · redrawn
Scene 07, before and after, both in the art style of the time. The claim was “coming off the bed.” Every viewer read the first version as a dog standing on it. Fresh viewers read the redraw as a departure. The set has since been redrawn twice, and this composition survived both unchanged.

Use, which is the check that takes features out

Anything I am not sure of ships switched off. It gets turned on in the hallway and either earns its place or goes.

Voice commands went after a couple of real sessions, for a reason no diagram could have shown and no AI raised. The timer that replaced them had every label rewritten after one session. A feature that survives that is one I have watched work, not one I expect to.