Voice assistant v1: spoken event announcements + push-to-talk voice commands - #721
Conversation
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 Report❌ Patch coverage is Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
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>
|
On-device smoke test (Pixel, debug build):
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 ( |
There was a problem hiding this comment.
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
TextToSpeechwrapper. - 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.
- 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
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
DxAlertNotifierdecode/QSO hooks:SNR_UNKNOWNsentinel)WpxPrefix+ worked-prefix check as the Add a "New Prefix" (WPX) decode highlight + filter for prefix chasers #695 highlight)VoiceAnnouncementDecisions, cloned fromAlertDecisions) 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 pureVoiceCommandParserand 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 (pureVoiceAnswerSelector). Each action gets a spoken echo; unknown speech gets a toast.Audio safety — the two hard rules from the TX-pipeline docs:
mutableIsTransmittingobserver hard-stops in-flight speech at key-up.SpeechRecognizerwould fight our capture, which already claimsVOICE_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 forRecognitionService/TTS_SERVICE, floating mic button rendered only when commands are enabled (zero layout shift, so the waterfallAndroidViewnever resizes).Known v1 limitations
Test plan
VoiceCommandParserTest(14),VoiceAnnouncementDecisionsTest(13),VoicePhrasesTest(14),VoiceAnswerSelectorTest(7),HamRecorderMicGateTest(4),VoiceCommandButtonLogicTest(7)testDebugUnitTest: BUILD SUCCESSFUL (entire existing suite + new tests)🤖 Generated with Claude Code