mobile

Voice entry capture on mobile — build scope

Status: SCOPED, not built. Tier-1 gap from MOBILE_WEB_GAP.md: the phone is our best voice-capture device and it's the one core-loop feature completely absent on mobile. Doubles as a marquee interaction for the Workout Window app (../product/WORKOUT_WINDOW_APP.md) — "talk about how the workout felt" beats typing on a treadmill.

The good news: the backend already exists and is device-agnostic

POST /api/journal/voice-transcribe (apps/backend/src/routes/journal.ts:3112) is a plain multipart endpoint — no web-specific assumptions:

  • Auth: authenticateUser (mobile already sends the bearer token via apiRequest).
  • Body: multipart/form-data, file field audio, optional language (^[a-z]{2}$).
  • Accepts webm / ogg / mp3 / m4a / wav; m4a is exactly what expo-av records on iOS, and AAC/.m4a is the sensible Android target too — so no format conversion.
  • Returns { text, duration_seconds }. It does not create an entry — the caller pre-fills the composer and the user edits before saving. Same contract the web composer uses.
  • Cost/limits: Whisper ~$0.006/min, 25 MB upload cap.

So this is a client-only build. Zero backend changes.

What the web version does (the flow to mirror)

apps/frontend/src/app/dashboard/entries/voice/page.tsx: idle → requesting → recording (duration counter) → transcribing → preview (editable transcript + auto-derived title) → save. Save POSTs the edited transcript to the normal create-entry endpoint. Auto-title = first sentence or first 80 chars (deriveTitle — worth copying verbatim for parity).

Mobile build plan

  1. Dependency: expo-av is already installed (~16.0.7) — use Audio.Recording (HIGH_QUALITY preset → .m4a/AAC). No new native module. (If we later want on-device dictation instead of upload, that's expo-speech recognition — out of scope; keep the Whisper round-trip for parity + accuracy.)
  2. Permissions: add mic usage strings — iOS NSMicrophoneUsageDescription, Android RECORD_AUDIO — in app.json (ios.infoPlist / android.permissions). Request at record time via Audio.requestPermissionsAsync(); handle denial.
  3. New screen screens/dashboard/VoiceEntryScreen.tsx mirroring the web state machine. Record → stop → read the file URI → upload as multipart: FormData with { uri, name: 'voice-note.m4a', type: 'audio/m4a' } under field audioPOST /api/journal/voice-transcribe → put text into an editable preview → on Save, hand off to the existing EntryEditor create path (reuse its save + query-invalidation; don't duplicate entry-creation logic).
  4. Entry point: a mic button on the Dashboard and/or in EntryEditor. Wire VoiceEntry into MainNavigator.
  5. Auto-title: port deriveTitle into @hivejournal/shared (or copy) so web + mobile derive identically.

Edge cases to handle (parity with web)

  • Permission denied → friendly prompt to enable in Settings.
  • Empty/near-silent transcript (text blank) → let the user re-record, don't save empty.
  • Upload/transcribe failure (non-2xx returns { error }) → surface message, keep the recording so they can retry without re-recording.
  • Very long recordings near the 25 MB cap → cap recording duration or warn.
  • Background/interruption (call comes in mid-record) → stop + preserve what we have.

Effort / risk

Small–medium, client-only, low risk — the endpoint is proven in production and format-compatible with expo-av out of the box. The only native surface is the mic permission (standard Expo config). No migration, no backend deploy.

Acceptance

Record on a physical iOS and Android device → transcript returns → edit → save → entry appears in the stream, identical to a typed entry. Denial + failure paths show a clear message and never create an empty entry.

VOICE ENTRY MOBILE — Docs | HiveJournal