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 viaapiRequest). - Body:
multipart/form-data, file fieldaudio, optionallanguage(^[a-z]{2}$). - Accepts
webm / ogg / mp3 / m4a / wav;m4ais exactly whatexpo-avrecords on iOS, and AAC/.m4ais 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
- Dependency:
expo-avis already installed (~16.0.7) — useAudio.Recording(HIGH_QUALITYpreset →.m4a/AAC). No new native module. (If we later want on-device dictation instead of upload, that'sexpo-speechrecognition — out of scope; keep the Whisper round-trip for parity + accuracy.) - Permissions: add mic usage strings — iOS
NSMicrophoneUsageDescription, AndroidRECORD_AUDIO— inapp.json(ios.infoPlist/android.permissions). Request at record time viaAudio.requestPermissionsAsync(); handle denial. - New screen
screens/dashboard/VoiceEntryScreen.tsxmirroring the web state machine. Record → stop → read the file URI → upload as multipart:FormDatawith{ uri, name: 'voice-note.m4a', type: 'audio/m4a' }under fieldaudio→POST /api/journal/voice-transcribe→ puttextinto an editable preview → on Save, hand off to the existing EntryEditor create path (reuse its save + query-invalidation; don't duplicate entry-creation logic). - Entry point: a mic button on the Dashboard and/or in EntryEditor. Wire
VoiceEntryintoMainNavigator. - Auto-title: port
deriveTitleinto@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 (
textblank) → 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.