This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

My parents walk every evening. Tonight they came back and told me about the sparrows they heard and the mogra flowers they saw. That's pretty much the whole idea behind what I built this week. Give them a small reason to look around, and let the phone stay at home.

Last week I built them something to protect them from their phones (Rakshak). This week I wanted something that gets them off their phones.

What I Built

Sair (सैर, it means "a stroll") is a walk companion that barely has a screen.

  1. At the door, Mom taps "Hear today's walk". Sair says one small thing to notice, slowly, twice. Something like "Notice one sound you like on your walk."
  2. Then the phone stays home. The app actually tells them "Leave me here. I'll be waiting when you're back." There are no maps, no step counts, no streaks and no notifications. It's about 30 seconds of screen before the walk and none during it.
  3. When they're back, Mom holds one button and just talks for up to a minute.
  4. A model running on our MacBook turns that voice note into a short diary entry, and the things they noticed show up as little chips, like "gulmohar · near the corner" or "koel · near the temple".
  5. The next day's mission builds on what they really noticed. For example: "On Tuesday you heard a koel near the temple. Is it there again?"

It's built for two people in their fifties and sixties who are fine with WhatsApp and UPI and who switch between English, Hindi and Hinglish in the same sentence.

The hard part wasn't the app. It was this: hallucinations pile up. If an AI journal makes up a bird, that's a bit annoying. If a walk companion with memory makes up a bird, it sends two people out the next evening to look for a bird that was never there. So Sair has one rule I didn't want to bend: nothing goes into the diary, or into memory, that nobody actually said.

Demo

I recorded this in the Android Emulator, talking to the real hub on my MacBook. Same app, same models, same pipeline. The voice note in the video is a text-to-speech stand-in, not my mom.

In order, you'll see Sair speaking the mission (that's Kokoro, running on the laptop), the "leave me here" screen, hold to talk, "Writing it down…", and then the diary entry with three checked facts and "Mission done".

At the door The mission, spoken twice Back home: hold to talk The diary entry
Door screen: Hear today's walk Mission playing: Notice one sound you like on your walk Recording a voice note Got it. Saved. Entry with three verified fact chips

The first real walk

Tonight was my parents' first walk with Sair. They heard sparrows and saw mogra flowers.

My parents' first walk with Sair

On the first walk

Code

Sair (सैर)

A screenless evening-walk companion for two parents. Before the walk, Sair speaks one small noticing mission at the door, twice. The phone stays home. Afterwards, a voice note of up to a minute becomes a short family diary entry, and the next mission builds only on what was actually said.

"On Tuesday you heard a koel near the temple. Is it there again?"

Everything runs on a MacBook at home. No inference, and no content or timing of any real walk, leaves the house.

Why it's built this way

Hallucinations compound. If a diary invents a bird, tomorrow's mission sends two people looking for a bird that was never there. Sair keeps the diary faithful with three pieces:

  • a small open model fine-tuned to write grounded entries (Qwen3.5-4B + LoRA, served by llama-server with a JSON grammar);
  • a grounding check in code: every fact must quote the transcript…

A quick map of the repo:

  • hub/ is the FastAPI and LangGraph hub (a journal graph and a mission graph) with SQLite. It only listens on loopback.
  • hub/grounding.py has the seven grounding checks, written as plain code.
  • hub/safety.py keeps missions safe with a verb allowlist, a blocklist and a check on where every noun came from.
  • training/ covers data generation, teacher labelling, LoRA training on Tinker and the export to GGUF.
  • eval/ is the evaluation harness, with bootstrap confidence intervals and the memory contamination chains.
  • web/ is the React PWA, matched pixel by pixel against the design.

There are 1,136 tests (unit, integration and privacy) and they all pass in CI.

How I Built It

Everything runs on one MacBook

It's an M3 Pro with 18 GB that sits on a shelf at home. The hub only binds to 127.0.0.1, and the phones reach it over a private Tailscale network using tailscale serve, with Funnel turned off.

Job Open model or tool Runs on
Transcribe the voice note Gemma 4 E2B through Ollama, straight from audio, temperature 0 MacBook
Plan tomorrow's mission Gemma 4 E2B MacBook
Write the diary entry Sair-Writer: Qwen3.5-4B with a LoRA I trained on Tinker, as a Q8_0 GGUF on llama.cpp with a GBNF grammar MacBook
Speak the mission Kokoro-82M, in its own process MacBook
Glue LangGraph (no persistent checkpointer), FastAPI and SQLite MacBook
Tell me a walk happened self-hosted ntfy, counts only, never any text MacBook

How Sair works: phones reach the MacBook over Tailscale; Gemma 4, Sair-Writer, the grounding checks, Kokoro and SQLite all run on it

The fact checking is code, not a prompt

Politely asking a model not to make things up doesn't work. So every entry goes through seven checks written in ordinary Python, in hub/grounding.py:

  1. Every fact has to quote the transcript exactly.
  2. The thing they saw, and where, has to be inside that quote.
  3. Hedges survive. "I think it was a koel" stays a maybe.
  4. Every concrete detail in the entry has to be backed by one of those facts.
  5. No new numbers, times or names.
  6. The entry has to look like an entry: 5 to 70 words, says who walked, no questions, no links.
  7. If it says the mission was done, that needs a quote too.

If an entry fails, the writer gets one more try. If it fails again, Sair writes a plain entry from a template that code builds. I'd rather have a boring true sentence than a lovely false one.

Memory only holds facts that passed the first three checks. The mission planner never sees the diary text, only those checked facts and the last five missions. Missions get checked too. Each one has to be a single task, at most 30 words, use an allowed verb, avoid anything risky, speak to them as "you", never mention a time of day, and only use nouns that came from a checked fact or a short list of safe starters.

Fine-tuning a small writer on Tinker

The stock Qwen3.5-4B, even with good examples in the prompt, put at least one made-up detail into 42% of its entries. I wanted something small enough to run next to Gemma on a laptop, and a lot more honest.

  • Data: synthetic voice-note transcripts made from generic seeds like parks, lanes, temples and markets. Nothing from my family went in. I split by seed, so the test seeds were never seen during training.
  • Teacher: Qwen3.5-397B on Tinker wrote the gold entries, and each one had to pass the same seven checks or it got replaced by the code template. That gave me 1,114 training rows.
  • Training: LoRA rank 32 on Qwen3.5-4B. I ran a small learning-rate sweep and then the full run, and picked epoch 2 because it had the lowest made-up-detail rate on the dev set.
  • Export: I merged the LoRA into the base weights in f32 and converted to GGUF. Q4_K_M failed my export check (its template fallback rate moved by 2.33 points and my limit was 2), so Sair ships Q8_0, which matched the Tinker checkpoint on every metric I gate on.
  • Cost: about $6.65 on the Tinker usage page. That's $4.95 for the 397B teacher and roughly $1.70 for the 4B sweeps, training and dev sampling.

Results

These numbers come from a held-out synthetic test set: 130 voice-note transcripts from 8 seeds the model never saw in training, scored by the same code checker. The confidence intervals are 95%, from a bootstrap that resamples whole seeds (10,000 times).

Writer Entries with a made-up detail Facts kept (recall) Fell back to the template
Gemma 4 E2B, zero-shot 20.0% 0.864 no safety net
Qwen3.5-4B base, few-shot 42.3% 0.743 no safety net
Sair-Writer (fine-tuned) 6.2% 0.939 no safety net
Gemma with the grounding check 0% 0.862 55.4%
Qwen base with the grounding check 0% 0.732 24.6%
Sair-Writer with the grounding check 0% 0.937 6.2%
  • Fine-tuning took made-up details from 42.3% down to 6.2% compared with its own base. That's 36.2 points lower, with a 95% interval of 22.9 to 49.3 points.
  • It didn't get there by saying less. Recall went up from 0.743 to 0.939 (19.7 points, interval 13.4 to 27.7), and hedges survived more often too, from 0.849 to 0.949.
  • With the grounding check switched on, every writer gets to 0% made-up details, because that's what the check enforces. The real difference is how often the safety net has to catch them. Sair-Writer falls back to the plain template on 6.2% of notes. Gemma falls back on 55.4%. That's the difference between a diary and a pile of templates.

Sair-Writer vs its base model and Gemma zero-shot: made-up details 42.3% vs 6.2%, facts kept 74.3% vs 93.9%, template fallback 24.6% vs 6.2%

What didn't work

I'd rather tell you than have you find it.

  • My contamination experiment came back null. I ran 7-day chains comparing memory built from diary text against memory built from checked facts, expecting the diary-text memory to leak made-up details into later missions. It didn't, for any writer. Diary-text memory gave 0 contaminated chains out of 20, and fact memory gave at most 1 out of 20, which was a number word like "one". It turns out Gemma's missions are generic enough that they rarely reuse details at all. I still think the rule is right, but this run doesn't prove it.
  • Hinglish voice notes currently come back from Gemma in Devanagari script, and then the writer falls back to the template. English works well. This is the first thing I'm fixing.
  • Speed: in my demo runs, an 18 to 20 second note was ready in about 20 to 25 seconds (I was checking every 5 seconds). I haven't measured it properly on real notes yet.
  • Every number above is synthetic. Real-walk numbers will come after real walks, and I'm not going to publish any before they exist.

Privacy: nothing about a walk leaves the house

A record of when two older people leave home and how long they're out is sensitive. So it isn't only what they say that stays at home. The timing stays home too.

  • The hub refuses to start on anything other than 127.0.0.1. Phones reach it through tailscale serve, and Funnel is off.
  • The only outbound connections are to the model servers on the same machine. Models are downloaded ahead of time and Hugging Face runs in offline mode.
  • Raw audio is deleted right after transcription. No audio or text ever goes into a log, a trace or a notification. When something fails, the reason is a short code.
  • A canary test pushes a marker phrase through the whole pipeline, and CI fails if that phrase shows up in any log, trace, notification or model-server log.
  • There's no telemetry in production. Sentry only runs in dev and eval on synthetic data, and the settings won't turn it on if any data path points at the real data folder.
  • No location, no streaks, no gamification. The app never asks for location, and CI scans the built app for geolocation, beacons and analytics.

Why Does Open Innovation Matter?

  • It can live in our house. With a closed API, every voice note, plus a timestamped record of when my parents go out, would sit on someone else's server. With open weights, inference never leaves the MacBook.
  • I could make it honest. I fine-tuned a 4B model for this one job and cut made-up details from 42% to 6%. I couldn't do that with a closed model this cheaply, and I definitely couldn't run the result offline.
  • I can check it. Open weights and a GBNF grammar let me force valid JSON and compare the export byte for byte. 105 of the 129 dev outputs were identical between the Tinker checkpoint and the Q8_0 GGUF.
  • It's free to run. About $6.65 to train, and then nothing per walk, on a laptop we already own.
  • It keeps working when the internet doesn't. I unplugged the router's internet connection and Sair kept working.

Prize Categories

  • Best Use of Tinker (Thinking Machines): a LoRA fine-tune of Qwen3.5-4B on Tinker with a measured gain over its own base. Made-up details went from 42.3% to 6.2%, recall from 0.743 to 0.939, and template fallback with the safety net from 24.6% to 6.2%.
  • Best Use of Gemma: Gemma 4 E2B runs locally through Ollama and does two jobs on every walk. It transcribes the voice note straight from the audio, with no separate speech model, and it plans the next mission from checked facts. It's also the zero-shot baseline I measured the fine-tune against.
  • Best Use of GitHub Copilot (GitHub Actions): every push runs the 1,136 unit, integration and privacy tests, scans the built app for outbound URLs, geolocation and analytics, and replays the cached evaluation against a committed baseline. If a change makes the diary less faithful, the build fails.

Built solo by @muaazsm. My parents get the credit for the walking.