Build write-up · 5 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 with the timer on: step 1 of 6, ring or knock once, in large type. Under it a strip with Pause, Next step in 10s and the seconds each step lasts, then 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, with Lucy trotting across a gold field above it. 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 tried for eighteen days, 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. What came back was 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, and by evening nobody remembers whether the barking is actually getting better or whether it was just a bad Tuesday.

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

Five decisions that shaped it

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

The obvious move is points, streaks, a satisfying progress bar. The unit here is the level, because the level is what the trainer talks in: 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 what you are trying to change: the Calm Door Greetings track with five stations, and activity cards for Doorbell Predicts Rewards and Stay While the Door Opens, each carrying its level and last practice.

    The program, in her levels

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

    The week, ready to hand to the trainer

  3. The Profile tab: Lucy’s card noting she gets over-aroused around arrivals, who practices with her, and Commands we use — move to bed reads “Go to bed,” stay behind boundary reads “Back,” remain in position reads “Stay,” above a line asking you to confirm the wording with the trainer first.

    The cues, in her words

All three run in her units, not mine: her levels, a week written to be handed over at the next lesson, and the exact words she says out loud, set once here and repeated by 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 is a record of what happened, not a reconstruction at the end of the day.

Speaking and listening, split

Speaking and listening got split, because they cost completely different things. Speaking runs on the device, needs no permission, and works with no signal, so the app reads the steps aloud by default. Listening shipped audio to a server, needed a microphone permission an installed web app does not reliably keep, and failed where practice actually happens.

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. Bundling them would have made the reliable half hostage to the fragile one, and when the fragile half came out, the reliable one did not move.

Listening came out after eighteen days

The split held up. The listening half did not, and it was not the microphone that took it out. A handler saying “next step” brightly at a phone is saying words, in the cue voice, at a dog who is being taught that words in that voice are for her. The commands avoided every cue and still shared the room with them, and no amount of practice was going to make that a fair thing to ask of Lucy.

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, and the same control sits in a strip on every step beside a pause and a live count, so the pace gets changed where it feels wrong rather than 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. Reading aloud stayed exactly as it was, because it asks nothing of anyone’s voice.

The clock never answers the rep question. It walks the steps and stops on the last one, because the log is built from that single observation, and a timer that answered it would be the reflex “went well” made automatic. One session with it rewrote every label to say timer or step, because “Move on by itself” was met with “move what on?”, and put the step’s number in front of each instruction the voice reads: the beat the handler wanted, and the cushion an idle audio route needed. Eighteen days to find out, a day to replace it, and the labels rewritten before that day was out.

Listening was a conversational interface, and it is worth being exact about how it failed. Not on recognition: the words were caught and the steps turned. It failed on everything else in the room. A modality can perform to specification and still be the wrong one, and no accuracy figure reports that — the number would have looked fine for all eighteen days. 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 was designed around where your hands actually are, and that turns out to be two different places.

A woman stands in a hallway with a phone in one hand and the leash in the other, looking down at a black dog with a white chest sitting at her feet. A treat pouch hangs at her hip, and the front door is visible past her.

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 at the edge of a living-room coffee table. Beyond it a woman crouches with both hands open and empty, facing a black dog with a white chest sitting 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, and both hands stay with the dog until the rep question needs a thumb.

Same session, two postures. The screen is built for both: one instruction in large type, the steps turning on their own, and the one tap that is left within reach of a thumb.

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: the same black coat, the same white chest, the same scruffy face, the collar with the round blue tag, and the same handler in the same room. From one screen to the next, almost nothing is allowed to change 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: 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. The illustrations have been redrawn twice from it, not from a chat window: each scene is a JSON spec that a Node script assembles and posts to OpenAI’s image model. Reference images in a set order, each with a named job: the style exemplar always first, a likeness sheet for the handler and one for Lucy, and for an adjacent moment the previous picture itself, so that nothing but the action may differ. Then the wording to put in front of it, and one sentence naming what the picture has to get right, which is what every round is checked against before anyone looks at it twice.

A chat window drew the first eight and worked. It also lost what makes a bad picture diagnosable: which references were attached, and in what order, the strongest lever on consistency there is. So they became ordered data, the prompt’s numbered list is generated from that same array, and the script refuses a wrong request before it costs anything. And when a redraw drifts, the fix is to go back to the last round that was right and change one thing, not to keep correcting the drifted one. Every round is on file, so that is a choice rather than a memory.

That is where the time went, and the four styles the app has had are the receipt for it.

  1. Painted era: a photographic living room in warm lamplight, the handler reading in an armchair, Lucy asleep on a gray mat on a patterned rug, 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 carries more detail than the step does, and at the 56 pixels the program map gives it, none of that detail 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 drawn flat on a tan wall over a warm wooden floor, the handler reading in a wooden-armed chair, Lucy asleep on a gray bed, nothing else in the room.

    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, a collar with a round blue tag and never a harness, in any image, ever. Still drawn by pasting paragraphs into a chat window, though, so no round was recorded and 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, which is the one thing nobody asked it 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. That is the whole argument for the pipeline: the same change a month earlier would have been another five days in a chat window, and this time it was an afternoon with every round on file.

One scene, drawn four ways in a month. What changed at the end was not the drawing but where the request came from: a chat window can make a picture, and it cannot repaint sixty-three of them between lunch and dinner.
  1. The first-launch welcome: an illustration of the handler and Lucy on her bed as a guest waits at the open door, above the heading Five minutes of practice at a time and a note that it supports your trainer and does not replace one.

    01 First launch, explained with a picture

  2. The get-ready screen for Doorbell Means Place, Level 1: the scene of the handler crouched at Lucy’s bed, the instruction Lead her to the bed with a treat in your hand, a checklist of what to have ready, 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 itself from all come out of the same script, in the same 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, and 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.

The acceptance criteria are falsifiable. 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.

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? A comprehension pilot, before I spent a real panel on it:

  • 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. Then 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” and every viewer read the first version as a dog standing on it. The redraw puts most of her off, one back foot still touching, and fresh viewers read a departure. The whole set has since been redrawn twice in the flat style it ships in now, and this composition survived both unchanged.

The third check is use, and it is the one that takes features out. Anything I am not sure of ships switched off, gets turned on in the hallway, and either earns its place or goes. Voice commands went, eighteen days after they arrived, for a reason no diagram could have shown, and 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.