Commit Graph

3 Commits

Author SHA1 Message Date
kutesir
4a05844ee3 Voice: fix audio-session leak causing insufficientPriority (561017449)
Decoded the device error: NSOSStatusErrorDomain 561017449 = '!pri' =
AVAudioSession insufficientPriority — the mic was denied because a session was
left active (which also blocks the keyboard's own dictation).

Root cause: the early-return guards (recognizer unavailable / invalid format) ran
AFTER setActive(true) but never deactivated, leaking an active session. Also the
.measurement + .duckOthers config is more prone to priority denials.

Fixes:
- Deactivate any stale active session before starting.
- Use .record / .default (least-exclusive recording config).
- stopEngine() now always releases the session (no early-out), and the bailout
  guards call it so the session is never left active.
- Map 561017449 to an actionable message (close other audio apps / restart).

Device-specific; not reproducible on the sim (blocks at the permission dialog).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:56:45 +03:00
kutesir
c9f0fd0777 Voice: harden startEngine against "Could not start recording"
Addresses the most common causes of the engine failing to start:
- Defensive teardown before starting (removeTap + stop + reset) — a leftover tap
  from a prior attempt makes installTap throw "already has a tap".
- Validate the input node's hardware format (sampleRate/channels > 0) and fail
  with a clear "No microphone input" message instead of throwing.
- Use inputNode.inputFormat(forBus:0) for the tap.
- Category .playAndRecord + [.duckOthers, .defaultToSpeaker] (more permissive
  than .record with .duckOthers).
- Surface the underlying NSError domain+code in the banner + log to pinpoint any
  remaining device-specific failure.

DEBUG-only KISANI_VOICE_START hook to drive the real engine on a sim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:33:16 +03:00
kutesir
eb7d563bbd feat(voice): wire existing mic button to live speech recognition
The mic icon in AddTaskSheet was a static Image with no action and no
speech service behind it. Full implementation:

SpeechRecognizer.swift (new):
- @MainActor ObservableObject wrapping SFSpeechRecognizer + AVAudioEngine
- Requests speech recognition then microphone permission in sequence
- Streams partial results into @Published transcript; final result stops engine
- Suppresses transient error 301 (no speech detected)
- reset() cleans up on sheet dismiss
- [VOICE] log trail at every step: tapped → granted → started → transcript → saved

TodayView.swift — AddTaskSheet:
- @StateObject private var speech = SpeechRecognizer()
- onChange(speech.transcript) writes transcript to rawText (task field)
- Mic Image → Button { speech.toggle() } with mic.fill + pulse animation while recording
- .safeAreaInset shows a red dismissible error banner on permission denial
- submit() stops recording before saving; logs task title
- .onDisappear resets speech state

project.yml:
- NSMicrophoneUsageDescription
- NSSpeechRecognitionUsageDescription

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
2026-06-24 19:48:00 +03:00