Don't double-label voices whose name already carries the quality tier

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
This commit is contained in:
2026-07-09 15:19:52 -04:00
parent 90deb582fe
commit 41226545e3
3 changed files with 24 additions and 9 deletions
+10
View File
@@ -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 {
+2 -9
View File
@@ -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)
}
}
+12
View File
@@ -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