Skip to main content
Case StudiesProjectsAbout
Case StudiesProjectsAbout
Product · AI — Case Study

CaloVoice: Voice-First Calorie Tracking

Designed and built a voice-powered calorie tracker that turns natural speech into structured food logs using an LLM pipeline, reducing meal logging from ~2 minutes to under 10 seconds.

5 min read·
Product DesignVoice UXAI / LLMReactFirebase
View case study document
<10s
Time to log
voice path vs. ~2 min manual
4
Fallback stages
never a dead-end error
0
JS dependencies
for state, animation, routing
My Role
Solo: product, design, engineering, deployment
Timeline
2025
Team
1 engineer
Impact
Meal logging from ~2 minutes to under 10 seconds
Platform
Mobile PWA
Stack
React 19 · Firebase · OpenRouter · Web Speech API · Zod
Status
Shipped · Live

Problem

Calorie tracking has a compliance problem, not a feature problem. Apps like HealthifyMe and MyFitnessPal require ~2 minutes per meal (searching databases, scrolling variants, adjusting servings), 3–4× a day. Most users quit within two weeks. The friction of logging outweighs the perceived value.

●Core Insight

People describe meals in natural language effortlessly. "Two rotis with dal and a bowl of curd" takes 3 seconds to say but 2 minutes to manually search and log. Voice isn't just a convenience. It's the difference between a habit that sticks and one that doesn't.

Solution

Voice-first logging: tap, speak, done. An LLM pipeline (Web Speech API → OpenRouter → CalorieNinjas) parses natural speech into structured food entries with calorie estimates in under 10 seconds.

Silence-as-submit: no 'Done' button during recording. 3.5s of silence after speech auto-submits. Silence is the punctuation.

Graceful degradation: if voice fails (unsupported browser, API error, empty parse), the user lands on a manual form silently, never an error dead-end.

Time-aware UI: the CTA adapts ('Log Breakfast' → 'Log Dinner'), and a pace tick on the calorie ring shows where you 'should' be at this hour.

Predictive coaching: after 2+ meals, the app projects end-of-day intake, 'At your current pace, you'll exceed by ~400 kcal. Consider a lighter dinner.'

Voice Pipeline

speech-pipeline.ts

Stage 1, Capture: Web Speech API captures speech continuously. Silence detection is transcript-driven: a 3.5s timer resets on transcript changes, not on audio amplitude. AudioContext runs in parallel for waveform visualization only. These are intentionally separate concerns.

Stage 2, Abort safety: Each parse() call creates a new AbortController and cancels the previous in-flight request. After every await, the code checks signal.aborted to discard stale results. The finally block only clears isProcessing if the request wasn't aborted.

Stage 3, LLM extraction: OpenRouter with JSON mode. Zod validates the response shape. Food items resolve via Promise.all with abort checks between each item.

Stage 4, Fallback chain: unsupported browser → manual form, empty transcript → manual form, LLM failure → manual form (food) or local regex parser (activity). The user never hits a dead-end error screen.

Key Decisions

  • PWA over native: MVP speed. Validates the voice interaction model before investing in native. V2 roadmap includes native for widget-based logging without opening the app.
  • No global state store: Firebase real-time listeners ARE the state layer. Components subscribe via hooks. No Redux, no Zustand, zero redundant state.
  • LLM via OpenRouter (model-agnostic): can switch between GPT-4, Claude, Llama without code changes. JSON mode ensures structured output. Local food dictionary reduces API calls for common items.
  • All CSS animations, no libraries: pure CSS with spring-eased curves. Sheets animate out before unmounting (240ms exit). Zero JS animation runtime cost.

Design System Enforcement

◆Enforcement Philosophy

Design token violations are a build error, not a code review comment. Two custom ESLint rules make it impossible to ship raw values to production.

.eslintrc: custom rules

no-raw-style-literals: flags numeric literals and px/rem/em/vh/vw values inside JSX style props. Forces all sizing through theme tokens.

no-color-literals: flags hex codes, rgb(), and hsl() in any component or page file. All colors must come from CSS custom properties.

Rules apply to src/**/*.{ts,tsx} but ignore theme/, services/, and hooks/, so token definitions and low-level math can still use raw values. Only UI files are constrained.

Vision & Roadmap

  • V1 (current): Voice-first logging, calorie ring with pace indicator, contextual insights, activity tracking.
  • V2: Conversational AI coach. Analyzes your day's data and guides real-time decisions ('You have 350 kcal left, skip the chips, try almonds with dark chocolate').
  • V3: Native app with home-screen widget. Tap mic, speak, done. No app open required. Background LLM processing.

Success Metrics

  • Voice vs. manual usage ratio
  • Time-to-log (target: under 10s for voice path)
  • LLM parse accuracy (edit rate before save)
  • 7-day and 14-day retention
  • Meals logged per day consistency

Tech Stack

React 19 · TypeScript · Firebase (Auth + Firestore) · OpenRouter (LLM) · CalorieNinjas API · Web Speech API · Vite · MUI · Zod · PWA

★Key Takeaway

Voice logging works not because it's faster. It's faster because it eliminates the translation step. Users already narrate meals in natural language; the old apps forced them to translate that narration into a database search. Removing the translation is what closes the habit gap, not the speed improvement itself.

NextCravia: Rate the Dish, Not the Restaurant

Built with intention, not templates. If you made it this far, thank you for reading thoughtfully.

© 2026 · Designed and built with care