DEV Community

Benjamin Fadina
Benjamin Fadina

Posted on

Tail Talk: reading the half of the conversation your dog is already having

DEV Weekend Challenge: Dog Days Edition Submission 🐕

This is a submission for Weekend Challenge: Dog Days Edition

What I Built

Tail Talk reads a dog's body language from a photo.

You upload a picture. Gemini walks the whole dog; ears, eyes, mouth, tail, posture, weight distribution, hackles ; and reports
what each part is saying, checks every rung of the canine ladder of aggression, and tells you what to do and what to avoid right now.

The thing that pushed me to build it: dogs warn us for a very long time before they growl. A lip lick. A yawn that isn't about
being tired. A head turned away. A crescent of white in the eye. These are the quiet rungs of a ladder, and it ends in a bite only
because everyone in the room missed the first eight steps. Most bite incidents aren't a dog "snapping without warning"; they're a dog that asked politely, repeatedly, in a language nobody around it spoke.

Tail Talk is a phrasebook for that language.

My goal was deliberately narrow: not a breed identifier, not a cuteness scorer. Those exist. I wanted an evidence-first behavioural report; one that shows its working, and admits what it cannot see.

Demo

🔗 Live app: https://tail-talk-rosy.vercel.app

Five sample photos are built in, so you can get a full report in one click without uploading anything.

Here's the read it gave on the Jack Russell sample — note that it caught the breed confounder on its own:

▎ Happy, energetic, and engaged · Safe to approach
▎ Arousal 60 · Comfort 85

▎ Tail: tail held high and upright above the backline — confident, high interest or excitement, typical for this breed

▎ Caveat: "Jack Russell and terrier breeds naturally carry their tails high and erect, which represents normal breed posture rather
▎ than high tension."

Code

How I Built It

GitHub logo benjaminsqlserver / tail-talk

Read your dog's body language from a photo. Built with Next.js and Google Gemini for the DEV Weekend Challenge: Dog Days Edition.

🐕 Tail Talk

Read the half of the conversation your dog is already having with you.

Upload a photo of a dog and Gemini walks the whole body — ears, eyes, mouth, tail, posture, weight, hackles — then checks every rung of the canine ladder of aggression and tells you what to do next.

🔗 Live demo: tail-talk-rosy.vercel.app

Built for the DEV Weekend Challenge: Dog Days Edition · Best use of Google AI


Why this exists

Dogs warn us for a long time before they growl. A lip lick, a yawn out of context, a turned head, a flash of white in the eye — these are the quiet rungs of a ladder that ends in a bite. The tragedy of most dog bites is that the dog did ask for space, repeatedly, and nobody in the room could read it.

Tail Talk is a reading aid for that vocabulary…

How I Built It

Next.js 16 (App Router) + TypeScript + Tailwind v4, @google/genai on gemini-3.7-flash, deployed on Vercel. Analysis runs in a Node
route handler, so the API key never reaches the browser, and uploaded photos live in memory for exactly one request; nothing is
stored.

The whole app is one Gemini call. Almost all of the engineering went into making sure that call can't wander.

The schema is the product

A vision model asked to "describe this dog's mood" will write you a lovely paragraph, and that paragraph will confidently mention a
tail that isn't in the frame. Free-form prose gives a model infinite room to hedge, embellish, and paper over what it can't
actually see.

So Gemini is bound to a strict responseSchema that forces it to commit:

  signals: {
    type: Type.ARRAY,
    description:
      "One entry for each body part in the enum. If a part is not visible in the photo, " +
      "set visible to false and say so honestly rather than guessing.",
    items: {
      type: Type.OBJECT,
      properties: {
        part: { type: Type.STRING, enum: [...BODY_PARTS] },
        visible: { type: Type.BOOLEAN },
        observation: str("What is literally visible…"),
        meaning: str("What that observation typically communicates…"),
      },
      required: ["part", "visible", "observation", "meaning"],
    },
  }
Enter fullscreen mode Exit fullscreen mode

That visible boolean is my favourite part of the whole build. It turns "I'm not sure" from something the model can bury in soft
language into a structural fact the UI can render — occluded body parts come back dimmed, with the model's own admission of what it
couldn't assess. Uncertainty becomes a feature of the interface instead of a failure of it.

The same trick drives the stress ladder: all ten signals, each explicitly present: true/false, with the instruction "True only if
you can actually see this sign. Do not infer it from mood." Without that clause a model back-fills stress signals to match a vibe
it already decided on.

Teaching it the domain traps

The system prompt encodes the mistakes a novice makes:

▎ Watch for breed confounders: curled tails, cropped or naturally erect ears, heavy jowls, brachycephalic faces and thick coats can
▎ imitate signals they do not mean.

▎ A relaxed, happy dog is a perfectly valid answer. Do not manufacture stress to seem insightful.

▎ Wagging is not the same as friendly. Judge tail height, stiffness and the whole body together.

That middle line is defensive: a model told its job is insight has every incentive to find tension in a dog that's simply having a
nice time, so "this dog is fine" needs to be explicitly blessed as a valid answer.

The breed-confounder instruction is what produced the Jack Russell caveat in the demo above. A high tail is a textbook arousal
signal — knowing when it isn't one is the difference between a party trick and something you'd actually trust.

Trusting the schema, but verifying it

Gemini honours the schema reliably but not always the "one entry per enum value" instruction. So normalise() rebuilds both
checklists against the canonical enum order server-side:

  const stressChecks = STRESS_SIGNS.map((sign) => {
    const found = raw.stressChecks?.find((s) => s.sign === sign);
    return { sign, present: found?.present ?? false, note: found?.note ?? "" };
  });
Enter fullscreen mode Exit fullscreen mode

The ladder always renders all ten rungs, always in order, whatever comes back. Structured output is a strong contract, not a
guarantee; treat it like an API response from a service you don't own.

Making the model say what it can't know

Every report ends with caveats, and they're required by the schema, not optional politeness. A photograph is one frozen sample of a
signal that only means anything in motion. An app that reads dog emotions and doesn't say that out loud is a worse app.

What I'd build next

Video. Body language is motion, and a three-second clip has strictly more signal than the best single frame; Gemini's video input
is the obvious next step. After that, a "watch this dog over time" mode: the same dog across many photos, tracking whether the
quiet stress signals are getting more or less frequent. That's the version that would actually help someone rehabilitating a
fearful rescue.

Prize Categories

Best Use of Google AI; Gemini is the entire analysis engine, via @google/genai with structured output.

Top comments (0)