From 41226545e3b24bcc7688cdc1a4d9db0d2a67b77f Mon Sep 17 00:00:00 2001 From: rzen Date: Thu, 9 Jul 2026 15:19:52 -0400 Subject: [PATCH] Don't double-label voices whose name already carries the quality tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Premium/enhanced voices ship with the tier in their name (e.g. "Ava (Premium)"), so appending "— Premium" produced "Ava (Premium) — Premium" in the voice picker. Move the label logic to SpeechSettings.displayLabel and skip the suffix when the name already contains the tier (still name-only for default voices). Deterministic string helper, unit-tested. Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh --- Workouts/Speech/SpeechSettings.swift | 10 ++++++++++ Workouts/Views/Settings/SettingsView.swift | 11 ++--------- WorkoutsTests/SpeechSettingsTests.swift | 12 ++++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Workouts/Speech/SpeechSettings.swift b/Workouts/Speech/SpeechSettings.swift index 41d7282..d9d877f 100644 --- a/Workouts/Speech/SpeechSettings.swift +++ b/Workouts/Speech/SpeechSettings.swift @@ -100,6 +100,16 @@ enum SpeechSettings { @unknown default: "Default" } } + + /// The picker label for a voice: its name, with the quality tier appended only when it + /// adds information. Default voices get no suffix (labeling every one "Default" is noise), + /// and enhanced/premium voices whose name already carries the tier — e.g. "Ava (Premium)" — + /// aren't labeled twice. + static func displayLabel(name: String, quality: AVSpeechSynthesisVoiceQuality) -> String { + guard quality != .default else { return name } + let label = qualityLabel(quality) + return name.localizedCaseInsensitiveContains(label) ? name : "\(name) — \(label)" + } } private extension AVSpeechSynthesisVoiceQuality { diff --git a/Workouts/Views/Settings/SettingsView.swift b/Workouts/Views/Settings/SettingsView.swift index 6616333..a564489 100644 --- a/Workouts/Views/Settings/SettingsView.swift +++ b/Workouts/Views/Settings/SettingsView.swift @@ -211,14 +211,6 @@ struct SettingsView: View { installedVoices.contains { $0.quality != .default } } - /// Most installed voices are default quality, so labeling every one "Default" is noise; - /// name only for those, and call out the enhanced/premium ones that are worth choosing. - private func voiceLabel(_ voice: AVSpeechSynthesisVoice) -> String { - voice.quality == .default - ? voice.name - : "\(voice.name) — \(SpeechSettings.qualityLabel(voice.quality))" - } - /// All spoken-instruction controls. The voice and the speed/pitch/volume adjustments also /// govern the library exercise Speak button; only the toggle gates the automatic workout cue. @ViewBuilder @@ -233,7 +225,8 @@ struct SettingsView: View { Picker("Voice", selection: $speechVoiceIdentifier) { Text("Automatic").tag("") ForEach(installedVoices, id: \.identifier) { voice in - Text(voiceLabel(voice)).tag(voice.identifier) + Text(SpeechSettings.displayLabel(name: voice.name, quality: voice.quality)) + .tag(voice.identifier) } } diff --git a/WorkoutsTests/SpeechSettingsTests.swift b/WorkoutsTests/SpeechSettingsTests.swift index a27113d..72d22f4 100644 --- a/WorkoutsTests/SpeechSettingsTests.swift +++ b/WorkoutsTests/SpeechSettingsTests.swift @@ -36,6 +36,18 @@ struct SpeechSettingsTests { } } + @Test func displayLabelAvoidsRedundantQualitySuffix() { + // Default voices: name only, no "— Default" noise. + #expect(SpeechSettings.displayLabel(name: "Samantha", quality: .default) == "Samantha") + + // Enhanced/premium voices get the tier appended when the name lacks it. + #expect(SpeechSettings.displayLabel(name: "Samantha", quality: .enhanced) == "Samantha — Enhanced") + + // …but not when the name already carries it (e.g. "Ava (Premium)"). + #expect(SpeechSettings.displayLabel(name: "Ava (Premium)", quality: .premium) == "Ava (Premium)") + #expect(SpeechSettings.displayLabel(name: "Daniel (Enhanced)", quality: .enhanced) == "Daniel (Enhanced)") + } + @Test func prosodyValuesClampToValidRanges() { let rateKey = SpeechSettings.rateKey let originalRate = UserDefaults.standard.object(forKey: rateKey) as? Double