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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user