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"
|
@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 {
|
private extension AVSpeechSynthesisVoiceQuality {
|
||||||
|
|||||||
@@ -211,14 +211,6 @@ struct SettingsView: View {
|
|||||||
installedVoices.contains { $0.quality != .default }
|
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
|
/// 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.
|
/// govern the library exercise Speak button; only the toggle gates the automatic workout cue.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -233,7 +225,8 @@ struct SettingsView: View {
|
|||||||
Picker("Voice", selection: $speechVoiceIdentifier) {
|
Picker("Voice", selection: $speechVoiceIdentifier) {
|
||||||
Text("Automatic").tag("")
|
Text("Automatic").tag("")
|
||||||
ForEach(installedVoices, id: \.identifier) { voice in
|
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() {
|
@Test func prosodyValuesClampToValidRanges() {
|
||||||
let rateKey = SpeechSettings.rateKey
|
let rateKey = SpeechSettings.rateKey
|
||||||
let originalRate = UserDefaults.standard.object(forKey: rateKey) as? Double
|
let originalRate = UserDefaults.standard.object(forKey: rateKey) as? Double
|
||||||
|
|||||||
Reference in New Issue
Block a user