Add spoken exercise instructions (on-device TTS)
Speak exercise setup and form cues aloud with AVSpeechSynthesizer: - Library detail: a speaker toolbar button reads the full reference aloud. - Active workout: an opt-in "Speak Exercise Cues" setting speaks a brief cue (Setup/Execution/Cues) when an exercise starts, hands-free. - Settings › Voice: pick the voice (auto-prefers an installed enhanced/ premium English voice), a premium-download nudge shown only while on a basic voice, and a Speed/Pitch/Volume sheet with Reset to Defaults. On-device and offline; ducks other audio rather than stopping it. iPhone only for now. Shared SpeechSettings is the single source of truth for the voice/prosody, read fresh per utterance so changes preview live. Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh
This commit is contained in:
@@ -28,6 +28,7 @@ struct ExerciseLibraryView: View {
|
||||
/// progression chart; the looping form-guide figure holds the bottom.
|
||||
struct ExerciseLibraryDetailView: View {
|
||||
@Environment(SyncEngine.self) private var sync
|
||||
@Environment(AppServices.self) private var services
|
||||
|
||||
let exerciseName: String
|
||||
|
||||
@@ -143,6 +144,25 @@ struct ExerciseLibraryDetailView: View {
|
||||
}
|
||||
.navigationTitle(exerciseName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if let info {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
let announcer = services.speechAnnouncer
|
||||
Button {
|
||||
if announcer.isSpeaking {
|
||||
announcer.stop()
|
||||
} else {
|
||||
announcer.speak(info.spokenScript(name: exerciseName, brief: false))
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: announcer.isSpeaking ? "stop.fill" : "speaker.wave.2.fill")
|
||||
}
|
||||
.accessibilityLabel(announcer.isSpeaking ? "Stop reading" : "Read instructions aloud")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Leaving the screen shouldn't keep narrating from a page you can no longer see.
|
||||
.onDisappear { services.speechAnnouncer.stop() }
|
||||
.sheet(item: $settingsEdit) { route in
|
||||
MachineSettingsSheet(exerciseName: exerciseName, initialSettings: route.initial) { updated in
|
||||
let targets = route.targetSplitIDs
|
||||
|
||||
Reference in New Issue
Block a user