high priority medium complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

The adapter exposes a SpeechToTextAdapter abstract interface with methods: requestPermission(), start(locale), stop(), pause(), cancel(), and a Stream<SpeechResult> results stream
requestPermission() returns a SpeechPermissionStatus enum (granted, denied, restricted, permanentlyDenied) and does not throw
start() throws a SpeechPermissionException if called without a prior granted permission status
Locale detection selects nb_NO (Norwegian Bokmål) if the device locale matches, otherwise falls back to en_US; the caller may override the locale explicitly
The results stream emits SpeechResult objects containing: transcript (String), isFinal (bool), and confidence (double 0-1)
On app background (AppLifecycleState.paused) the adapter automatically calls stop() and releases the audio session so other apps can use the microphone
On app foreground (AppLifecycleState.resumed) the adapter does NOT auto-restart — the UI layer decides whether to resume recording
no-speech timeout (configurable, default 5 seconds of silence) emits a SpeechError with code noSpeechDetected and stops recording
Permission-denied state surfaces a SpeechError with code permissionDenied and a localised message guiding the user to open iOS Settings
The adapter is completely unaware of any specific feature (post-session report, notes, etc.) — it is pure shared infrastructure
Unit tests cover the permission flow state machine, locale selection logic, lifecycle hooks, and error emission paths using a mocked speech_to_text plugin

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
speech_to_text Flutter package
iOS SFSpeechRecognizer (via plugin)
Android SpeechRecognizer (via plugin)
data models
SpeechResult
SpeechError
SpeechPermissionStatus
performance requirements
First interim transcript must be emitted within 500ms of the user starting to speak
Adapter must release the audio session within 200ms of receiving the app background lifecycle event
Memory usage must not grow unboundedly during a long recording session — the results stream must not buffer more than the last 100 interim results
security requirements
NSMicrophoneUsageDescription and NSSpeechRecognitionUsageDescription strings must be present in Info.plist with Norwegian Bokmål rationale text for the Blindeforbundet pilot build
Speech transcripts must never be sent to any third-party service — only the device-side speech recogniser is used (no cloud STT API calls)
The adapter must not log transcript content in any error or debug output to prevent inadvertent capture of sensitive conversation content

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define SpeechToTextAdapter as an abstract class and provide FlutterSpeechToTextAdapter as the concrete implementation. This allows tests to inject a fake adapter and allows future swapping of the underlying plugin. Use a StreamController internally; expose only the stream, not the controller. Subscribe to WidgetsBindingObserver for lifecycle hooks — register the adapter as an observer in its constructor and deregister in a dispose() method.

The Riverpod provider for this adapter should be a Provider so any feature can read it via ref.watch without triggering unnecessary rebuilds. For the no-speech timeout, use a Timer that starts when recording begins and resets on each interim result; on timeout, cancel the timer, emit the error, and call stop(). Important: Blindeforbundet workshop notes explicitly state that recording during the actual peer-mentor visit is unwanted — ensure the adapter's start() method cannot be called from any screen shown during an active session (enforce this via a guard in the upstream report screen, not in the adapter itself).

Testing Requirements

Write unit tests using flutter_test with a MockSpeechToText class that simulates the speech_to_text plugin interface. Test matrix: (1) requestPermission granted → start() succeeds, (2) requestPermission denied → start() throws SpeechPermissionException, (3) locale nb_NO selected when device locale is Norwegian, (4) locale en_US selected as fallback for non-Norwegian locale, (5) caller-specified locale overrides detection, (6) 5-second silence → noSpeechDetected SpeechError emitted and recording stops, (7) AppLifecycleState.paused → stop() called automatically, (8) interim results streamed correctly, (9) final result emitted with isFinal=true. Integration test: on a physical iOS device, confirm the permission dialog appears, granting it allows recording, and the transcript appears in the stream.

Component
Speech-to-Text Adapter
infrastructure medium
Epic Risks (2)
medium impact high prob technical

Flutter's speech_to_text package behaviour differs meaningfully between iOS and Android — microphone permission flows, locale availability, background audio session interference, and partial-result timing all vary. Inconsistent behaviour could make voice input unreliable for the primary audience (visually impaired peer mentors on iOS VoiceOver).

Mitigation & Contingency

Mitigation: Test speech-to-text-adapter on physical iOS and Android devices from the start, not just simulators. Write platform-specific test cases for permission flows and locale detection. Design the adapter's public interface to be platform-agnostic so that a native bridge could replace the package if needed.

Contingency: If speech_to_text proves unreliable on a platform, implement a native-speech-api-bridge (already identified in the component catalogue) as a drop-in replacement within the adapter, keeping the external interface unchanged so no UI code needs to change.

medium impact medium prob dependency

The coordinator task queue notification mechanism is not fully specified. If the queue system is owned by another team or uses an external service, way-forward-task-service may block on an undefined integration contract, delaying this epic.

Mitigation & Contingency

Mitigation: Define the task queue notification interface as an abstract Dart interface early in the epic. Implement a stub that writes a flag to the database so coordinator list queries can detect new tasks, deferring the real notification integration to a later epic.

Contingency: If the queue integration remains undefined at implementation time, ship way-forward-task-service with database persistence only and add a TODO-flagged notification hook. Coordinators will still see items on next page load; push notification delivery is deferred.