Privacy-first SMS & messaging app with an offline voice assistant for hands-free drive mode.
Forked from QKSMS and rebuilt to keep private message storage and speech processing local.
SilentPulse is a drop-in replacement for the Android stock SMS app with an on-device, hands-free voice assistant. Optional weather, navigation ETA, stock prices, and AI questions use explicitly permitted online services.
Android speech is restricted to the on-device recognizer and installed offline TTS voices, with no cloud fallback. SilentPulse cannot firewall a separate Google/system process or guarantee that it never makes unrelated network requests; that requires device-level controls.
Say "Computer" at any time while the assistant is running. The Vosk on-device keyword spotter hears it silently (zero beeps, minimal CPU). The mic then hands off to Android's on-device Speech Recognizer and plays a single beep so you know it is ready.
| Category | Example phrases |
|---|---|
| Time | "What time is it" · "What time is it in Tokyo" · "Time in New York" |
| Weather | "What's the weather" · "Weather in Berlin" · "Weather on the corridor" |
| Navigation | "Navigate to 123 Main Street" · "Directions to the airport" · "Stop navigation" |
| Drive time | "How long to downtown" · "Drive time to Chicago" |
| Notifications | "Read my notifications" · "Read my email" — then: Skip / Reply / Repeat / Dismiss / Stop |
| Music | "Play music" · "Play [artist/song]" · "Resume" |
| Audiobooks | "Listen to book" · "Open voice" |
| Stock prices | "Stock price Microsoft" · "Stock price AAPL" · "Stock price crude oil" · "Stock price gold" |
| General questions | "What is the capital of France?" · "Who invented the telephone?" |
| App control | "Open Maps" · "Close Spotify" |
| Cross-app logging | "Computer, Microcore, log weight 220 pounds" |
| Help | "Help" · "What can you do?" |
Stock lookup requires a command beginning with "stock price" (then a company, ticker or supported asset). Ordinary "what is..." / "how much..." questions and bare "price of..." requests no longer enter the stock handler merely because they contain a company name or words such as gold, coin or share.
Phase 1 — Vosk keyword spotter runs continuously, constrained to the grammar ["computer", "[unk]"]. It reads raw PCM via AudioRecord — zero beeps, zero SpeechRecognizer restarts, minimal battery drain.
Phase 2 — When Vosk hears the wake word, it releases the mic and hands off to the selected offline STT engine. Android recognition requires the API 31+ on-device recognizer, not merely EXTRA_PREFER_OFFLINE; unavailable offline recognition is reported rather than falling back to the generic/cloud-capable recognizer. Vosk and Whisper remain local alternatives.
On Android 13+, SilentPulse checks the installed recognition language when the service supports that optional query. An unsupported or timed-out query does not disable a working on-device recognizer: the same offline recognizer is used, and its actual language or permission errors are reported. A check that explicitly reports a missing language still blocks recognition. SilentPulse never requests automatic model downloads or switches to the generic recognizer.
The transcript then flows through a routing chain:
| Handler | Backend |
|---|---|
TimeHandler |
IANA timezone DB — fully offline, 150+ city aliases, Levenshtein fuzzy match |
WeatherCommandHandler |
open-meteo.com / wttr.in — open source, no API key |
NavigationCommandHandler |
OSRM for ETA; launches OsmAnd / Maps for turn-by-turn |
DriveTimeHandler |
router.project-osrm.org |
NotificationReaderHandler |
Reads notification shade; inline reply via RemoteInput |
MusicCommandHandler |
MediaBrowser / MediaController, local library |
StockQueryHandler |
Yahoo Finance unofficial API |
GeneralQueryHandler |
DuckDuckGo instant answers → Wikipedia → Brave fallback |
BraveSearchHandler |
Optional AI search summariser (user-supplied API key) |
CommandRouter |
Cross-app broadcast dispatch with Levenshtein fuzzy app-name matching |
Android TTS must have an installed voice for the requested language that does not require a network connection; a missing or network-only voice is rejected rather than used as a fallback. The bundled Kokoro/Sherpa backend is temporarily blocked because its native error diagnostics can expose input text even with debugging disabled. Its saved models/preferences remain intact, and it is not silently replaced by another engine.
Only approved companion apps can be voice targets. SilentPulse:
- Discovers only Grafium (
com.grafium.app) and Microcore (com.microcore.microcore), requiring a signing identity matching SilentPulse - Strips the app name from the command and dispatches an
EXECUTE_COMMANDbroadcast - The target app replies with a package-scoped
TTS_REPLYbroadcast; SilentPulse requires a fresh, one-use request nonce, approved recipient identity, and pending request before speaking - One command, one reply, then back to the wake word. Companion follow-up hints never activate another listening turn or implicitly route the next utterance to that app
Example: "Computer, Microcore, log weight 220 pounds" — SilentPulse routes "log weight 220 pounds" to the Microcore health app, which logs the measurement and replies "Logged weight at 220 pounds."
Microcore requires voice-bridge protocol 2: EXTRA_SESSION_ID identifies each one-shot request, and a separate fresh EXTRA_REPLY_NONCE is echoed in the reply. Its internal protocol remains compatible, but SilentPulse does not continue companion conversations. Incomplete commands must be repeated as a new, complete wake-word command. The receiver requires the signature-level com.silentpulse.messenger.permission.VOICE_COMMAND permission. Older global-broadcast Microcore bridges are rejected before private commands are sent. Grafium's existing package-scoped protocol uses a fresh nonce in EXTRA_SESSION_ID; its signed manifest supplies local command-help text instead of accepting unauthenticated legacy schema replies.
Private app-directed commands are not forwarded to online AI when a companion is unavailable. Cancel/stop, timeout, speech failure, and service shutdown revoke pending reply capabilities. These controls authorize SilentPulse's integrations; they do not audit every behavior of the approved companion apps.
Weight, blood-pressure, fasting, and calorie questions default to local handling even without an app name. This includes polite logging requests, spelled-out measurements, and general health questions: SilentPulse does not guess whether a health question is personal. When no companion route matches, it asks for an explicit Microcore command instead of searching the web or inventing a health result. To intentionally search online for general health information, explicitly name an online provider (for example, "Brave, what is fasting"). Do not include private health readings in online requests.
The currently installed Microcore bridge still has limitations: paired blood-pressure logging, fasting-duration queries, and calories-burned answers are not implemented. Its calorie voice answer reports remaining intake allowance, not energy burned. Use complete commands with explicit units for weight. If Microcore says it is waking up, that acknowledges a queued command, not a completed write; open Microcore and check the result before repeating a logging command.
Core principle: private content must not be forwarded to online providers without the user's explicit request.
network_security_config.xml limits the app's framework-managed TLS trust:
base-configtrusts zero certificate authorities — any unlisted domain fails at TLS handshake- Only explicitly whitelisted privacy-respecting domains can connect:
| Domain | Purpose |
|---|---|
open-meteo.com |
Weather (open source, no key) |
wttr.in |
Weather fallback (open source, no key) |
api.duckduckgo.com |
General instant answers (no key) |
en.wikipedia.org |
Knowledge lookups (no key) |
nominatim.openstreetmap.org |
Geocoding (OpenStreetMap, no key) |
router.project-osrm.org |
Drive-time routing (OSRM open source) |
query1/2.finance.yahoo.com |
Stock prices (unofficial) |
api.search.brave.com |
AI search summariser (optional — user-supplied key) |
This is not a device-wide egress firewall. It does not govern other applications, Android speech/font/backup services, or arbitrary native networking. Online AI questions and public-city weather requests are intentional network operations; the provider necessarily sees the request and connection metadata.
SilentPulse disables automatic cloud backup and device-to-device extraction of its app data. An update does not delete any existing remote backup or change Android's separate system SMS-backup settings.
The UI uses platform fonts and does not request downloadable Google fonts or emoji metadata. Debug builds retain content-free diagnostics; release app diagnostics are disabled. Messages, contact details, transcripts, authentication URLs, and request capabilities must not appear in diagnostic logs.
AI conversation cookies/session behavior is unchanged by these privacy fixes. A configurable conversation inactivity window is a separate, deferred design discussion.
- No Firebase of any kind (no Crashlytics, Analytics, FCM, Remote Config, Performance)
- No app-initiated Google font downloads or Play Services telemetry SDKs
- No crash or event reporting (Sentry, Datadog, Amplitude, Mixpanel, Segment…)
- No advertising SDKs
- No cloud TTS or cloud STT — all speech processing is on-device only
- No log shipping — debug diagnostics stay local, and release diagnostics are disabled
- No A/B testing or feature flag services
- No referral or attribution tracking
| Variant | Description |
|---|---|
noAnalytics (default) |
No Firebase, no analytics, no crash reporting. This is the only variant that should be distributed. |
withAnalytics |
Legacy flavor kept for historical reference — do not distribute. |
The four-icon control widget requests a 2-column, 1-row footprint for the notification reader, next notification, voice assistant, and stop-speaking controls. Tight horizontal padding keeps all four icons visible at 2x1, while horizontal resizing also supports 3x1 and 4x1 for larger touch targets. Actual grid sizing depends on your launcher.
After updating, long-press an existing widget and use its side handles to shrink its width, or its top/bottom handles to reduce its height to one row. If your launcher keeps the old minimum size or does not show resize handles, remove and re-add the widget.
Add SilentPulse World Clock from your launcher's widget picker, search for a city or IANA time zone, choose white or black text for your wallpaper, and tap Save clock. Its background is fully transparent.
Each clock starts at 2 columns by 1 row and can now be resized down to 1×1. Long-press the clock and drag its side handles inward; four compact clocks can fit on a four-column home screen. The time scales to the available width, with no app-added horizontal padding; longer city labels may be shortened. Add as many independent instances as your launcher allows, with a different city on each. Tap the time or city to change its city or text color. Actual grid dimensions and outer padding depend on your launcher; remove and re-add an old widget if it retains the previous minimum size.
For shared city names, select the state/country explicitly. Lafayette, Indiana uses America/Indiana/Indianapolis (Eastern), while Lafayette, Louisiana uses America/Chicago (Central). The old unqualified Lafayette entry meant Louisiana; existing saved clocks are relabeled clearly without silently changing their time zone. The picker also distinguishes other common shared names, including Portland, Birmingham, Richmond, Jackson, Columbus, San Jose, London, Paris and La Paz. Searches accept qualifiers such as Lafayette IN or Portland Maine.
All 214 explicit aliases were cross-checked against Open-Meteo/GeoNames city records and IANA zone links on 2026-09-30. Their existing mappings correspond to real places; the Lafayette problem was an ambiguous name, not a universal one-hour offset. Audited region metadata also prevents voice queries such as Lafayette Indiana, El Paso TX or Perth Australia from discarding the qualifier or using an over-broad state/country default. IANA-derived entries use their named zone directly. Actual daylight-saving and political-rule updates still depend on the device's time-zone database.
Timekeeping works entirely offline using Android's time-zone database and automatically follows daylight-saving changes and the system's 12/24-hour format. The launcher updates the time without a background service or periodic alarms.
A small, transparent weather icon sits between the time and city, with no temperature text in the compact layout. Sunny/clear-night, partly cloudy, cloudy, fog, rain, sleet, snow and storm icons are bundled vector drawings, not downloaded images.
Weather uses Open-Meteo (no API key; CC BY 4.0). Only the selected city and its geocoded coordinates are requested; no device location, messages or contacts are used. Cities are matched to the clock's time zone rather than silently using a different location. Predefined country/region shortcuts resolve to and display a named representative city, such as Japan → Tokyo. The picker prioritizes exact city matches and removes duplicate region shortcuts; real city names such as Seattle remain distinct. Conditions are cached per city and refreshed about hourly using WorkManager; Android can delay background work. Tap the weather icon to refresh manually. A question-mark cloud means weather is pending or unavailable, not a guessed forecast. UTC and unresolved city aliases still work as clocks but may have no weather. Removing the last clock cancels refresh work and clears the separate weather cache.
Qualified city weather additionally checks country and state/region, so a Louisiana or Tennessee result cannot supply weather for Lafayette, Indiana. Offline qualifier metadata is derived from the Open-Meteo geocoding API, based on GeoNames data; no runtime network lookup is needed to select a clock time zone.
Add SilentPulse Stocks from the widget picker. Enter Yahoo Finance symbols in your preferred order (for example AAPL, MSFT, ^DJI, GC=F, BTC-USD), choose a layout and save. Each widget has an independent watchlist, background, text color and refresh interval.
The main navigation drawer now includes Stocks, beside the other app features. It lists installed stock widgets and opens the settings for the selected instance, with a shortcut to Settings backup. If there are no stock widgets, it explains how to add one; remembered watchlists and imported presets are not erased. Returning to the page refreshes the widget list, and a removed widget cannot be accidentally edited.
- Compact: one line per symbol with price/currency and signed daily absolute/percentage change. Positive changes are green, negative changes red, and unchanged or unknown changes neutral.
- Dense: larger 14sp base text in tighter, borderless rows showing ticker, price and signed percentage change. Enable Two columns to put two stocks on each row; hiding currency labels allows two columns at narrower sizes. Large fonts or narrow widgets fall back to one column. Cached quotes are marked with
*. - Cards: ticker, company name, price, daily changes and quote timestamp, with optional gradient-filled intraday line charts underneath. Wide widgets can show two card columns; narrower widgets and larger accessibility fonts use one column.
Two-column layouts reserve a 12dp gutter, rather than allowing the columns to touch. Gains and losses use more vivid green/red, with separate high-contrast shades for light versus dark/OLED backgrounds.
Single-line layouts have no added vertical row margins or font padding: Compact uses a 16dp minimum instead of the previous 32dp, while Dense uses 18dp. Rows still expand for larger fonts rather than clipping text. Group separators and borders use a visible 2dp line in the theme's secondary text color, not the faint app-divider color.
Each widget has a Ticker text size slider, from 80% to 180% of its layout's normal size (100% by default). It scales ticker, price/change and supporting row text without changing the rest of the app. Existing widgets update immediately, and the value is included in settings backups and imported presets. Text no longer auto-shrinks against the selected size; large values or labels may need a wider widget, and two-column layouts adapt to the larger text.
All stock widgets now follow SilentPulse's theme by default, including a true #000000 background in OLED mode. Light/dark/system theme changes update the widgets without fetching quotes. Disable Follow SilentPulse theme to choose manual light/dark colors instead.
All layouts support a fully transparent background, including the toolbar and individual stock cells; transparency takes precedence over the theme background. With theme following disabled, choose light text for dark wallpaper or dark text for light wallpaper. Turn off Show currency labels to omit USD, EUR, etc. from prices; each quote keeps its original currency (there is no conversion), and accessibility descriptions retain it. Existing layout, transparency and watchlist choices are retained.
Use Add group to insert a [Heading] after the current editor line, then list its instruments underneath. Choose horizontal separators, a border around each group, or headings alone. Groups work with every layout, and two-column rows never mix instruments from different groups. Move complete sections in the editor to reorder groups. Group names stay local and are never sent to the quote provider.
Append a minus inside a heading, such as [Commodities-], to put a horizontal rule beside the group name on the same row, including in Headings only mode. No separate separator row is added above the heading. The displayed heading omits the trailing minus; the marker is retained in saved watchlists and settings backups. Explicit border mode still draws the group outline.
Find stocks, funds, indices or commodities opens a full-screen themed picker. Typing filters built-in shortcuts locally; tap Search Yahoo or the keyboard search key to look up other instruments by name, issuer or ticker. For example, searching Fidelity can return mutual funds such as FXAIX (Fidelity 500 Index Fund), along with Fidelity ETFs and similarly named companies. Results show the instrument name, ticker, type and exchange where available. The picker is not limited to the built-in catalog, but coverage and quote availability depend on Yahoo; it cannot guarantee every instrument in existence. Search and ticker verification use the same already-whitelisted Yahoo domains, with no key or account.
Built-in index shortcuts include S&P 500 (^GSPC), Dow (^DJI), Nasdaq Composite (^IXIC), Nasdaq 100 (^NDX) and Russell 2000 (^RUT). Names such as snp500, gold, crude oil, natural gas and 10 year treasury also resolve locally in the watchlist editor. Gold (GC=F) and crude oil (CL=F) are futures, not spot prices; bond coverage includes ETFs such as BND/TLT and Treasury futures such as ZN=F, not individual CUSIP/ISIN bonds. Currency-pair tickers such as EURUSD=X are supported. Use $TICKER when a literal ticker conflicts with an asset name (for example $CORN is the ETF, while corn resolves to corn futures).
Only an explicit online search sends the text in the search box to Yahoo; typing/local filtering does not. Search queries are not logged, and no group names or other app content are appended to them. Old search responses are discarded when the query changes. Online failure is displayed rather than reported as zero matches.
Verify and save watchlist checks new or previously unverified tickers against Yahoo, then shows their identities for confirmation. Unknown symbols or unavailable responses block the save instead of silently adding unusable tickers. Previously verified instruments already in that widget can be retained offline when only changing its layout or groups.
For an existing widget, appearance controls and refresh interval save and refresh the widget immediately. Unchecking Show currency labels therefore remains applied when leaving with Back; no verification or network request is needed for a display-only change. Unsaved ticker/group-content edits are kept separate and still require Verify and save. A new widget must be verified and saved before it is configured.
Tap an individual stock cell to open a themed details page showing its full name, instrument type/exchange when available, quote time and daily change. Each column opens its own ticker; group headings and empty grid cells do not select a stock. Long-press stays available for the launcher's move/resize/remove controls.
Details include 1-day, 5-trading-day, 1-month and 1-year trend charts, defaulting to one month. Daily quote changes always compare against the previous trading close, never the start of a longer chart range. Historical chart colors describe the selected range, dates use the phone's time zone, and missing samples remain gaps.
The details page also requests related headlines from Yahoo, showing only stories explicitly tagged with that ticker. Some instruments, particularly funds, have no matching articles. Headline loading failures do not block the quote or chart. Tapping a headline opens its returned HTTPS Yahoo Finance article URL in the external browser; SilentPulse does not fetch article bodies, images, ads or scripts. The browser and publisher can use cookies/tracking and are outside the app's network controls. No new in-app domain, WebView scraper or dependency is required.
Yahoo's tags can represent a passing mention, not the main subject: a T-Mobile story may also be tagged AAPL. All tagged stories remain available. An info icon beside the headline marks cases where a conservative local check did not find the selected company/asset name or ticker in the headline. Tap the icon to see Yahoo's actual ticker tags and the explanation; it does not open the article. This is not an analysis of the article body or a guarantee about its primary subject, and some relevant headlines can use a different name.
The widget starts at 4×2, resizes horizontally and vertically, and scrolls to show the rest of the watchlist. Use the gear to edit symbols, change their order or switch layouts, and the refresh button for a manual update. Automatic updates can run every 15, 30 or 60 minutes; Android may defer them. Manual requests are throttled to once per minute per symbol, and overlapping watchlists share a local quote cache.
Quotes and five-minute chart samples use Yahoo Finance's unofficial, keyless chart endpoint, shared with the voice assistant through feature/stocks/data/YahooStockClient. No extra app, account, API key, WebView scraping, browser cookies, new network domain or dependency is needed. Only requested ticker symbols and connection metadata go to Yahoo; no messages, group names, contacts or device location are sent. Verification requests quotes; saving enables automatic refresh.
This is not a live trading feed: quotes may be delayed, rate-limited or unavailable, and charts show the latest session returned by Yahoo rather than invented out-of-hours data. Mutual funds may publish NAV only once daily and have no intraday line chart; the widget does not invent samples. Daily changes use the provider's previous close, not the first chart point. Cards show the provider's quote time; the header shows when requests were checked. Failed or overdue cached quotes are labeled, missing prices never become zero, and missing chart samples remain gaps. The unofficial API can change without notice. Holdings, price alerts and trading are not included.
Removing the last stock widget cancels scheduled work and clears its separate quote cache, but keeps the last-used watchlist and its groups in private app storage. Adding a new widget pre-fills the most recently saved or removed list, so it does not need to be typed again. Multiple installed widgets remain independent; deleting one does not change the others.
Open Settings → Settings backup to export a versioned JSON file using the local document picker. This is separate from the existing SMS Backup & restore, which backs up messages rather than preferences.
The settings file includes global accent colors (including exact signed ARGB values), light/dark/system/OLED mode, fonts/text size, automatic colors, supported global messaging/notification/voice preferences, every stock watchlist and group (including [Commodities-] markers), all stock display/refresh choices, the remembered watchlist, and every world clock's city, time zone and text color. Effective appearance defaults are included even if never explicitly changed. Previously imported widget presets are included in later exports.
On import, the entire file is validated before any preference writes, and a verified private pre-import snapshot is saved for Restore pre-import snapshot. Only supported settings present in the file are replaced; absent sections and unrelated settings remain untouched. Empty or identical imports do not discard the previous recovery snapshot. Importing does not start microphone/location services, grant permissions or trigger network refreshes.
Widget configurations are portable presets, not launcher IDs. Existing widgets are not overwritten by matching numeric IDs. After importing, add or configure a stock/clock widget and choose Imported presets from its overflow menu, review it, then save. Stocks retain their explicit Verify and save flow. Launcher placement and size are not included.
Files are readable and unencrypted and can contain watchlists, city labels and a message signature; keep them in private, device-local storage. Messages, contacts, per-conversation overrides, blocked senders, logs, caches, API keys, cookies/authentication/session data, permissions, running states, automatic deletion, custom ringtone/model files and web-AI settings are excluded. The backup page lists the supported settings and exclusions in detail.
Requires Java 17 and NDK 28.2.13676358.
The Gradle wrappers disable Realm's build-time analytics. Set REALM_DISABLE_ANALYTICS=true in the environment when building directly through an IDE or a system Gradle installation too.
# Debug APK — sideload via ADB for testing
./gradlew :presentation:assembleNoAnalyticsDebug
# Release APK — uses production keystore if secrets are set, otherwise debug key
./gradlew :presentation:assembleNoAnalyticsReleaseBefore sideloading, check the built APK's 16 KB compatibility:
python3 scripts/check-apk-alignment.py path/to/built.apkThis checks every packaged arm64-v8a and x86_64 library's ELF load segments and, for uncompressed libraries, its ZIP alignment. CI runs the same check on debug and release APKs before publishing, so incompatible prebuilt dependencies cannot silently ship. Rebuilding our own native code alone does not fix a misaligned third-party library.
Realm 10.19.0 and Vosk 0.3.75 (with its transitive JNA 5.18.1 AAR) supply compatible native libraries. The Realm update from 10.18.0 retains database file format v23 and requires no app schema change.
adb install -r -t path/to/built.apkUse the existing signing key for in-place updates. If Android reports a signature mismatch, stop rather than uninstalling or clearing app data.
The bundled MMS MIME classifier uses the app-owned com.silentpulse.messenger.mms namespace. Keep it separate from Android's framework MMS classes: a framework class with the same name can shadow the bundled implementation and crash optimized builds when image or video messages are displayed. This compatibility fix requires no message-database reset or attachment deletion.
Every push to master runs Build and Release:
- Installs NDK
28.2.13676358 - Builds both
noAnalytics-debugandnoAnalytics-releaseAPKs - Falls back to a freshly generated debug keystore when production signing secrets are absent
- Publishes a GitHub pre-release with both APKs attached
Always check the Actions tab after pushing. If a run fails, expand the failed step. Common failure modes are documented in .github/copilot-instructions.md.
Set in Settings → Secrets → Actions on the repository:
| Secret | Value |
|---|---|
KEYSTORE_BASE64 |
base64 -w0 release.jks |
KEYSTORE_PASSWORD |
Store password |
KEY_ALIAS |
Key alias |
KEY_PASSWORD |
Key password |
android-smsmms/ MMS library (fork of klinker41/android-smsmms)
common/ Shared utilities
data/ Realm database, repositories
domain/ Use-cases / business logic
presentation/ UI + Voice Assistant
└─ feature/
├─ assistant/
│ ├─ VoiceAssistantService.kt Foreground service — wake-word → route → TTS
│ ├─ VoskWakeWordDetector.kt Phase 1: always-on Vosk keyword spotter
│ ├─ CommandRouter.kt Cross-app broadcast dispatch
│ ├─ TimeHandler.kt Offline timezone resolution (150+ cities)
│ ├─ WeatherCommandHandler.kt Weather (open-meteo / wttr.in)
│ ├─ NavigationCommandHandler.kt Turn-by-turn + ETA
│ ├─ DriveTimeHandler.kt OSRM drive-time queries
│ ├─ NotificationReaderHandler.kt Hands-free notification reading + reply
│ ├─ StockQueryHandler.kt Real-time stock / crypto prices
│ ├─ GeneralQueryHandler.kt DuckDuckGo + Wikipedia + Brave fallback
│ ├─ BraveSearchHandler.kt Optional AI search summariser
│ └─ MusicCommandHandler.kt Local media playback control
└─ drivemode/
├─ DriveModeService.kt Core drive-mode notification reader
├─ AndroidSttEngine.kt On-device SpeechRecognizer wrapper
└─ VoiceActivityDetector.kt VAD for push-to-listen
Released under the GNU General Public License v3.0 (GPLv3) — see LICENSE.
Original QKSMS by Moez Bhatti. SilentPulse additions copyright their respective authors.