feat: add TTS voice guidance during GPS-tracked workouts

Voice announcements run entirely in the Android foreground service
(works with screen locked). Configurable via web UI before starting
GPS: time-based or distance-based intervals, selectable metrics
(total time, distance, avg/split/current pace), language (en/de).

Also syncs workout pause/resume state to the native service — pausing
the workout timer now freezes the Android-side elapsed time, distance
accumulation, and TTS triggers.

Includes TTS engine detection with install prompt if none found, and
Android 11+ package visibility query for TTS service discovery.
This commit is contained in:
2026-03-25 10:13:12 +01:00
parent d40a7fe7c5
commit 1a2ec40e7a
5 changed files with 660 additions and 21 deletions
+55 -4
View File
@@ -13,11 +13,25 @@ export interface GpsPoint {
timestamp: number;
}
export interface VoiceGuidanceConfig {
enabled: boolean;
triggerType: 'distance' | 'time';
triggerValue: number;
metrics: string[];
language: string;
voiceId?: string;
}
interface AndroidBridge {
startLocationService(): void;
startLocationService(ttsConfigJson: string): void;
stopLocationService(): void;
getPoints(): string;
isTracking(): boolean;
getAvailableTtsVoices(): string;
hasTtsEngine(): boolean;
installTtsEngine(): void;
pauseTracking(): void;
resumeTracking(): void;
}
function checkTauri(): boolean {
@@ -98,7 +112,7 @@ export function createGpsTracker() {
}
}
async function start() {
async function start(voiceGuidance?: VoiceGuidanceConfig) {
_debugMsg = 'starting...';
if (!checkTauri() || isTracking) {
_debugMsg = `bail: tauri=${checkTauri()} tracking=${isTracking}`;
@@ -130,7 +144,8 @@ export function createGpsTracker() {
const bridge = getAndroidBridge();
if (bridge) {
_debugMsg = 'starting native GPS service...';
bridge.startLocationService();
const ttsConfig = JSON.stringify(voiceGuidance ?? {});
bridge.startLocationService(ttsConfig);
// Poll the native side for collected points
_pollTimer = setInterval(pollPoints, POLL_INTERVAL_MS);
_debugMsg = 'native GPS service started, polling...';
@@ -175,6 +190,37 @@ export function createGpsTracker() {
track = [];
}
function getAvailableTtsVoices(): Array<{ id: string; name: string; language: string }> {
const bridge = getAndroidBridge();
if (!bridge) return [];
try {
return JSON.parse(bridge.getAvailableTtsVoices());
} catch {
return [];
}
}
function hasTtsEngine(): boolean {
const bridge = getAndroidBridge();
if (!bridge) return false;
return bridge.hasTtsEngine();
}
function installTtsEngine(): void {
const bridge = getAndroidBridge();
bridge?.installTtsEngine();
}
function pauseTracking(): void {
const bridge = getAndroidBridge();
bridge?.pauseTracking();
}
function resumeTracking(): void {
const bridge = getAndroidBridge();
bridge?.resumeTracking();
}
return {
get track() { return track; },
get isTracking() { return isTracking; },
@@ -186,7 +232,12 @@ export function createGpsTracker() {
get debug() { return _debugMsg; },
start,
stop,
reset
reset,
getAvailableTtsVoices,
hasTtsEngine,
installTtsEngine,
pauseTracking,
resumeTracking
};
}