Skip to content

Voice assistant v1: spoken event announcements + push-to-talk voice commands - #721

Merged
patrickrb merged 4 commits into
devfrom
feat/voice-assist
Aug 2, 2026
Merged

Voice assistant v1: spoken event announcements + push-to-talk voice commands#721
patrickrb merged 4 commits into
devfrom
feat/voice-assist

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Summary

Opt-in hands-free layer aimed at mobile/POTA passenger operation and accessibility (everything defaults off).

Announcements (TTS, per-event toggles) — ride the existing DxAlertNotifier decode/QSO hooks:

  • "K 1 A B C calling you, minus 5" (callsigns spelled letter-by-letter so engines don't read them as words; SNR clause dropped on the SNR_UNKNOWN sentinel)
  • "QSO with K 1 A B C logged"
  • "New country: …" / "New prefix: …" (CQ-gated; prefix via the same WpxPrefix + worked-prefix check as the Add a "New Prefix" (WPX) decode highlight + filter for prefix chasers #695 highlight)
  • Pure decision layer (VoiceAnnouncementDecisions, cloned from AlertDecisions) with namespaced per-session dedup; a suppressed announcement does not burn its dedup key.

Voice commands (offline, push-to-talk — not always-listening) — small keyword grammar answer / call CQ / stop / skip / log it (plus variants), parsed by a pure VoiceCommandParser and mapped onto the same entry points the UI buttons use (callStation, userResetToCQ + setActivated, forceLogAndMoveOn). "Answer" picks the newest decode addressed to me, else the caller-queue head (pure VoiceAnswerSelector). Each action gets a spoken echo; unknown speech gets a toast.

Audio safety — the two hard rules from the TX-pipeline docs:

  1. TTS never plays while the rig is keyed (it would be mixed into rig audio and transmitted): the announcer refuses to start an utterance during TX and a mutableIsTransmitting observer hard-stops in-flight speech at key-up.
  2. The mic button is disabled whenever FT8 RX holds an Android audio-capture session (phone mic or Android-routed USB input — SpeechRecognizer would fight our capture, which already claims VOICE_RECOGNITION/UNPROCESSED). Direct-libusb USB and LAN audio leave it available; the gate is runtime truth (HamRecorder.isPhoneMicInUse()), not config.

Also: new Voice Assistant settings category (5 toggles, config-table persistence incl. hydration arms), <queries> entries for RecognitionService/TTS_SERVICE, floating mic button rendered only when commands are enabled (zero layout shift, so the waterfall AndroidView never resizes).

Known v1 limitations

  • QSO-complete announcements that fire while the 73 over is still keyed are dropped, not deferred.
  • No capture-pausing: with phone-mic RX the command button is simply unavailable by design.
  • en-US recognition/speech only; per-session announcement dedup (same as notification alerts).
  • Needs an on-device smoke test (TTS engine presence, recognizer availability, behavior across a real TX cycle) before merge — not yet exercised on hardware.

Test plan

  • 59 new unit tests, all pure JVM: VoiceCommandParserTest (14), VoiceAnnouncementDecisionsTest (13), VoicePhrasesTest (14), VoiceAnswerSelectorTest (7), HamRecorderMicGateTest (4), VoiceCommandButtonLogicTest (7)
  • Full testDebugUnitTest: BUILD SUCCESSFUL (entire existing suite + new tests)
  • ktlint/detekt: no new violations beyond repo-wide pre-existing patterns; baseline untouched
  • On-device smoke test (see above)

🤖 Generated with Claude Code

Opt-in hands-free layer for mobile/POTA passenger operation and
accessibility. Announcements (TTS, per-event toggles) ride the existing
DxAlertNotifier decode/QSO hooks: station calling you (with SNR), QSO
logged, new-DXCC CQ, new-prefix CQ. Callsigns are spelled letter-by-letter
so engines don't read them as words.

Two hard audio-safety rules from the TX pipeline docs are enforced:
- TTS never plays while the rig is keyed (it would be mixed into the TX
  audio and transmitted): the announcer refuses to start an utterance
  during TX, and a mutableIsTransmitting observer hard-stops in-flight
  speech at key-up. Suppressed announcements don't burn their dedup key.
- The push-to-talk mic button is disabled whenever FT8 RX holds an
  Android audio-capture session (phone mic, or Android-routed USB input) -
  SpeechRecognizer would fight our capture. Direct-libusb USB and LAN
  audio leave it available.

Commands are a small offline keyword grammar (answer / call CQ / stop /
skip / log it) mapped onto the same entry points the UI buttons use
(callStation, userResetToCQ + setActivated, forceLogAndMoveOn), with a
pure newest-caller selector for "answer" and a spoken echo of each action.

New Voice Assistant settings category (5 toggles, config-table persistence
with hydration arms); <queries> entries for RecognitionService and
TTS_SERVICE. 59 new unit tests, all pure JVM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 6.77966% with 220 lines in your changes missing coverage. Please review.
✅ Project coverage is 35.98%. Comparing base (3fa9982) to head (48044f9).

Files with missing lines Patch % Lines
...o/ks3ckc/ft8af/ui/components/VoiceCommandButton.kt 13.00% 107 Missing ⚠️
...in/radio/ks3ckc/ft8af/ui/settings/VoiceSettings.kt 0.00% 75 Missing ⚠️
...lin/radio/ks3ckc/ft8af/ui/components/FT8AFIcons.kt 0.00% 25 Missing ⚠️
...n/radio/ks3ckc/ft8af/ui/settings/SettingsScreen.kt 0.00% 8 Missing ⚠️
...app/src/main/kotlin/radio/ks3ckc/ft8af/FT8AFApp.kt 0.00% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #721      +/-   ##
============================================
- Coverage     36.21%   35.98%   -0.23%     
  Complexity      289      289              
============================================
  Files           245      247       +2     
  Lines         30040    30276     +236     
  Branches       3942     3989      +47     
============================================
+ Hits          10879    10895      +16     
- Misses        18812    19032     +220     
  Partials        349      349              
Flag Coverage Δ
android 17.56% <6.77%> (-0.14%) ⬇️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...app/src/main/kotlin/radio/ks3ckc/ft8af/FT8AFApp.kt 0.45% <0.00%> (-0.01%) ⬇️
...n/radio/ks3ckc/ft8af/ui/settings/SettingsScreen.kt 2.63% <0.00%> (-0.09%) ⬇️
...lin/radio/ks3ckc/ft8af/ui/components/FT8AFIcons.kt 0.30% <0.00%> (-0.03%) ⬇️
...in/radio/ks3ckc/ft8af/ui/settings/VoiceSettings.kt 0.00% <0.00%> (ø)
...o/ks3ckc/ft8af/ui/components/VoiceCommandButton.kt 13.00% <13.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Device smoke test caught the button only appearing/disappearing after an
app restart: the Composable read GeneralVariables.voiceCommandsEnabled as
a plain static, which nothing invalidates. Add the house-pattern LiveData
mirror (mutableVoiceCommandsEnabled) updated by the settings toggle and
config hydration, and observe it from VoiceCommandButton. Verified on
hardware: toggling now shows/hides the button immediately both ways.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@patrickrb

patrickrb commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

On-device smoke test (Pixel, debug build):

  • App launches clean, no crashes in logcat.
  • Settings → Voice Assistant renders all 5 toggles; TX-safety footnote present.
  • voiceCommandsEnabled persists across force-stop + relaunch (hydration arm verified).
  • Mic-arbitration gate works on real hardware: with FT8 RX capturing via the phone mic, the button renders dimmed and tapping it shows "Voice commands unavailable: the phone mic is capturing FT8 receive…".
  • Found + fixed live: the button originally only appeared/disappeared after app restart (non-reactive static read). Now backed by a LiveData mirror (6d8cdf5) — verified on hardware that toggling shows/hides it immediately both ways.

Still untested on hardware (needs a human voice + a keyed rig): actual speech-recognition round trip, TTS announcement audio, and announcer cutoff across a real TX cycle.

Testing aside (no action needed): debug builds expose a second launcher activity (androidx.car.app.activity.CarAppActivity, the debug-only AAOS scaffolding kept when Android Auto was stripped), so monkey -c LAUNCHER randomly launches a blank screen on a phone — use am start -n radio.ks3ckc.ft8af/.ComposeMainActivity when driving the debug app via adb.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in “Voice Assistant v1” layer to FT8AF, providing spoken announcements (TTS) for key decode/QSO events and push-to-talk voice commands (STT) that map onto existing transmit/log actions, with explicit safety gates to prevent audio from leaking into TX and to avoid Android audio-capture contention.

Changes:

  • Introduces voice assistant core logic (announcement decisions/dedup + command parsing/selection + phrase builders) and an Android TextToSpeech wrapper.
  • Adds a floating push-to-talk mic button, new settings category/toggles, config persistence + hydration updates, and required Android 11+ package-visibility <queries>.
  • Adds a comprehensive pure-JVM unit test suite for the new decision/parse/selection/gating logic and config hydration.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/components/VoiceCommandButtonLogicTest.kt Tests extracted button gating + echo phrase mapping.
ft8af/app/src/test/java/com/k1af/ft8af/wave/HamRecorderMicGateTest.java Tests pure mic-capture gate used to disable STT while AudioRecord is held.
ft8af/app/src/test/java/com/k1af/ft8af/voice/VoicePhrasesTest.java Tests exact spoken phrase builders and SNR sentinel handling.
ft8af/app/src/test/java/com/k1af/ft8af/voice/VoiceCommandParserTest.java Tests keyword parsing/normalization and priority rules.
ft8af/app/src/test/java/com/k1af/ft8af/voice/VoiceAnswerSelectorTest.java Tests “answer” candidate selection ordering and null/blank handling.
ft8af/app/src/test/java/com/k1af/ft8af/voice/VoiceAnnouncementDecisionsTest.java Tests announcement gating, priority, and dedup “claim” semantics.
ft8af/app/src/test/java/com/k1af/ft8af/database/DatabaseOprConfigHydrationTest.java Adds hydration tests to ensure voice command toggle updates static + LiveData mirror.
ft8af/app/src/main/res/values/strings_compose.xml Adds settings UI strings and voice command/TTS feedback strings.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/VoiceSettings.kt New Compose settings screen for voice toggles + persistence writes.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/SettingsScreen.kt Adds “Voice Assistant” category and routing to the new settings screen.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/VoiceCommandButton.kt Implements floating push-to-talk button, STT invocation, parsing, action mapping, and echo.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/FT8AFIcons.kt Adds a microphone icon for the new UI control.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/FT8AFApp.kt Places the mic button as an overlay to avoid resizing waterfall/layout.
ft8af/app/src/main/java/com/k1af/ft8af/wave/MicRecorder.java Exposes whether capture is using direct-libusb USB path (no AudioRecord).
ft8af/app/src/main/java/com/k1af/ft8af/wave/HamRecorder.java Adds pure + runtime mic-capture-in-use logic for STT gating.
ft8af/app/src/main/java/com/k1af/ft8af/voice/VoicePhrases.java Pure phrase builders for announcements and command echoes.
ft8af/app/src/main/java/com/k1af/ft8af/voice/VoiceCommandParser.java Pure keyword parser for offline-ish, fixed-grammar voice commands.
ft8af/app/src/main/java/com/k1af/ft8af/voice/VoiceAnswerSelector.java Pure selection logic for “answer” targeting.
ft8af/app/src/main/java/com/k1af/ft8af/voice/VoiceAnnouncer.java Android TextToSpeech wrapper with TX gate + bounded pending queue + dedup key set.
ft8af/app/src/main/java/com/k1af/ft8af/voice/VoiceAnnouncementDecisions.java Pure decision + dedup claim/key helpers for voice announcements.
ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java Wires TX->TTS hard-stop safety, exposes mic-in-use and “answer best caller” helper.
ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java Adds persisted voice toggles and LiveData mirror for command button visibility.
ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java Hydrates new voice settings, including updating the LiveData mirror via postValue.
ft8af/app/src/main/java/com/k1af/ft8af/alert/DxAlertNotifier.java Adds voice announcement path alongside existing decode/QSO hooks.
ft8af/app/src/main/AndroidManifest.xml Adds <queries> for RecognitionService and TTS service (Android 11+ package visibility).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ft8af/app/src/main/res/values/strings_compose.xml Outdated
patrickrb and others added 2 commits August 2, 2026 10:18
- Suppress the recognizer-error toast for ERROR_CLIENT: it's what
  cancel() (a deliberate second tap) emits, so toasting it made a
  user-initiated cancel look like a failure. Mapping extracted to the
  pure voiceErrorToastRes() and covered by tests.
- VoiceAnnouncementDecisions.norm() now uppercases with Locale.ROOT so
  dedup keys are stable regardless of device locale (Turkish dotted-I
  regression test added).
- Mic-gate strings no longer claim only the "phone mic" is the blocker -
  the gate also covers Android-routed USB input, and the wording now
  says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	ft8af/app/src/test/java/com/k1af/ft8af/database/DatabaseOprConfigHydrationTest.java
@patrickrb
patrickrb merged commit d01be83 into dev Aug 2, 2026
17 checks passed
@patrickrb
patrickrb deleted the feat/voice-assist branch August 2, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants