Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Tests/AppShellSupportTests/MacAppPreferencesStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ final class MacAppPreferencesStoreTests: XCTestCase {
userDefaults.set(true, forKey: "wispr.settings.aiProcessingEnabled")
userDefaults.set(false, forKey: "wispr.settings.launchOnLoginEnabled")
userDefaults.set(false, forKey: "wispr.settings.automaticallyCheckForUpdates")
userDefaults.set(true, forKey: "wispr.settings.autoResolveVoiceModelConflicts")
userDefaults.set("test-model", forKey: "wispr.settings.selectedAIModelID")
userDefaults.set(85.0, forKey: "wispr.settings.soundEffectsVolume")
userDefaults.set(
Expand All @@ -38,6 +39,7 @@ final class MacAppPreferencesStoreTests: XCTestCase {
XCTAssertTrue(snapshot.aiProcessingEnabled)
XCTAssertFalse(snapshot.launchOnLoginEnabled)
XCTAssertFalse(snapshot.automaticallyCheckForUpdates)
XCTAssertTrue(snapshot.autoResolveVoiceModelConflicts)
XCTAssertEqual(snapshot.selectedAIModelID, "test-model")
XCTAssertEqual(snapshot.soundEffectsVolume, 85)
XCTAssertEqual(snapshot.remoteProviders.count, 1)
Expand All @@ -57,6 +59,7 @@ final class MacAppPreferencesStoreTests: XCTestCase {
XCTAssertTrue(snapshot.automaticallyCheckForUpdates)
XCTAssertEqual(snapshot.selectedLanguage, .german)
XCTAssertEqual(snapshot.translationOutputMode, .original)
XCTAssertFalse(snapshot.autoResolveVoiceModelConflicts)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ final class SettingsSearchPresentationTests: XCTestCase {
XCTAssertEqual(presentation.results.map(\.id), ["advanced.uninstall"])
}

func testSearchFindsVoiceModelCompatibilitySettings() {
let presentation = SettingsSearchPresentation(searchText: "kompatibilität", language: .german)

XCTAssertEqual(presentation.results.map(\.id), ["speech.language"])
}

func testSearchDoesNotExposePersonalTextOrUnknownTerms() {
let presentation = SettingsSearchPresentation(searchText: "private dictation text", language: .german)

Expand Down
70 changes: 70 additions & 0 deletions Tests/AppShellSupportTests/VoiceModelCapabilityPolicyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ final class VoiceModelCapabilityPolicyTests: XCTestCase {
catalog: models, installedFiles: files)?.id, standard.id)
}

func testPreservingSuggestionsKeepEveryCurrentCapabilityAndPreferInstalledModels() {
let current = model("whisper.nano.en")
let standard = model("whisper.standard")
let choices: [VoiceModelCapabilityChoice] = [
.translation(.english), .liveText, .quality(.accurate),
]

let suggestions = policy.suggestedModels(
preserving: choices, currentModel: current, currentLanguage: .english,
catalog: models, installedFiles: [standard.localFileName!]
)

XCTAssertEqual(suggestions.first?.id, standard.id)
XCTAssertTrue(suggestions.allSatisfy { candidate in
choices.allSatisfy { policy.supports($0, model: candidate, currentLanguage: .english) }
})
}

func testLanguageSuggestionSupportsBothCurrentAndRequestedLanguage() {
let englishNano = model("whisper.nano.en")
let standard = model("whisper.standard")
Expand All @@ -60,6 +78,58 @@ final class VoiceModelCapabilityPolicyTests: XCTestCase {
installedFiles: ["german-only.bin"])?.id, standard.id)
}

func testLanguageSuggestionsPrioritizeRequestedLanguageOverCurrentLanguage() {
let englishNano = model("whisper.nano.en")
let germanModel = VoiceModelDescriptor(
id: "german-only", providerID: VoiceProviderID.whisperCpp.rawValue,
displayName: "German only", languageCode: "de", languageScope: .multilingual,
supportsTranslationToEnglish: false,
supportsAutomaticLanguageDetection: false,
speedScore: 9, accuracyScore: 3, sizeLabel: "S",
installState: .downloadable, localFileName: "german-only.bin"
)

let suggestions = policy.suggestedModels(
for: .language(.german), currentModel: englishNano,
currentLanguage: .german, catalog: [englishNano, germanModel],
installedFiles: ["german-only.bin"]
)

XCTAssertEqual(suggestions.map(\.id), [germanModel.id])
}

func testAutoQualityProfileIsSupportedWithoutQualityProfiles() {
let parakeet = model("parakeet.multilingual")

XCTAssertTrue(policy.supports(.quality(.auto), model: parakeet, currentLanguage: .auto))
XCTAssertFalse(policy.supports(.quality(.accurate), model: parakeet, currentLanguage: .auto))
}

func testSupportedLanguagesFollowDescriptorCapabilities() {
let parakeet = model("parakeet.multilingual")

XCTAssertEqual(policy.supportedLanguages(for: parakeet), [.auto])
XCTAssertEqual(
Set(policy.supportedLanguages(for: model("whisper.standard"))),
Set(DictationLanguage.allCases)
)
XCTAssertEqual(policy.suggestedLanguage(for: model("whisper.nano.en"), currentLanguage: .german), .english)
}

func testAutomaticResolutionNeverSelectsAnUndownloadedModel() {
let parakeet = model("parakeet.multilingual")
let standard = model("whisper.standard")

XCTAssertNil(policy.bestInstalledSuggestion(
for: .liveText, currentModel: parakeet, currentLanguage: .auto,
catalog: [parakeet, standard], installedFiles: []
))
XCTAssertEqual(policy.bestInstalledSuggestion(
for: .liveText, currentModel: parakeet, currentLanguage: .auto,
catalog: [parakeet, standard], installedFiles: [standard.localFileName!]
)?.id, standard.id)
}

func testEffectiveModelUsesInstalledLanguageOverrideLikeSessionBuilder() {
let englishNano = model("whisper.nano.en")
let standard = model("whisper.standard")
Expand Down
3 changes: 3 additions & 0 deletions apps/macos/AppShell/MacAppPreferencesStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct MacAppPreferencesSnapshot {
let translationOutputMode: TranslationOutputMode
let visibleMenuBarLanguages: [String]
let performanceProfile: DictationPerformance
let autoResolveVoiceModelConflicts: Bool
let selectedVoiceProviderID: String
let selectedVoiceModelID: String
let voiceLanguageOverrides: [VoiceLanguageOverride]
Expand Down Expand Up @@ -76,6 +77,7 @@ struct MacAppPreferencesStore {
visibleMenuBarLanguages: userDefaults.stringArray(forKey: Keys.visibleMenuBarLanguages)
?? DictationLanguage.allCases.filter { $0 != .auto }.map(\.rawValue),
performanceProfile: enumValue(Keys.performanceProfile, default: .auto),
autoResolveVoiceModelConflicts: bool(Keys.autoResolveVoiceModelConflicts, default: false),
selectedVoiceProviderID: userDefaults.string(forKey: Keys.selectedVoiceProviderID)
?? LocalVoiceModelCatalog.defaultProviderID,
selectedVoiceModelID: userDefaults.string(forKey: Keys.selectedVoiceModelID)
Expand Down Expand Up @@ -248,6 +250,7 @@ struct MacAppPreferencesStore {
static let translationOutputMode = "wispr.settings.translationOutputMode"
static let visibleMenuBarLanguages = "wispr.settings.visibleMenuBarLanguages"
static let performanceProfile = "wispr.settings.performanceProfile"
static let autoResolveVoiceModelConflicts = "wispr.settings.autoResolveVoiceModelConflicts"
static let selectedVoiceProviderID = "wispr.settings.selectedVoiceProviderID"
static let selectedVoiceModelID = "wispr.settings.selectedVoiceModelID"
static let voiceLanguageOverrides = "wispr.settings.voiceLanguageOverrides"
Expand Down
20 changes: 17 additions & 3 deletions apps/macos/AppShell/MacAppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,14 @@ final class MacAppState: ObservableObject {
}
}

@Published var autoResolveVoiceModelConflicts: Bool {
didSet {
userDefaults.set(
autoResolveVoiceModelConflicts,
forKey: UserDefaultsKeys.autoResolveVoiceModelConflicts)
}
}

@Published var selectedVoiceProviderID: String {
didSet {
if oldValue != selectedVoiceProviderID { voiceSelectionRevision &+= 1 }
Expand Down Expand Up @@ -999,12 +1007,16 @@ final class MacAppState: ObservableObject {
}

var selectedVoiceModelLanguageOptions: [DictationLanguage] {
[.auto] + DictationLanguage.allCases.filter { $0 != .auto }
guard let model = activeVoiceModel ?? selectedVoiceModel else {
return [.auto] + DictationLanguage.allCases.filter { $0 != .auto }
}
let supported = VoiceModelCapabilityPolicy().supportedLanguages(for: model)
return supported.filter { $0 == .auto } + supported.filter { $0 != .auto }
}

var menuBarLanguageOptions: [DictationLanguage] {
[.auto] + DictationLanguage.allCases.filter {
$0 != .auto && (visibleMenuBarLanguages.contains($0.rawValue) || $0 == selectedLanguage)
selectedVoiceModelLanguageOptions.filter {
$0 == .auto || visibleMenuBarLanguages.contains($0.rawValue) || $0 == selectedLanguage
}
}

Expand Down Expand Up @@ -1104,6 +1116,7 @@ final class MacAppState: ObservableObject {
static let translationOutputMode = "wispr.settings.translationOutputMode"
static let visibleMenuBarLanguages = "wispr.settings.visibleMenuBarLanguages"
static let performanceProfile = "wispr.settings.performanceProfile"
static let autoResolveVoiceModelConflicts = "wispr.settings.autoResolveVoiceModelConflicts"
static let selectedVoiceProviderID = "wispr.settings.selectedVoiceProviderID"
static let selectedVoiceModelID = "wispr.settings.selectedVoiceModelID"
static let voiceLanguageOverrides = "wispr.settings.voiceLanguageOverrides"
Expand Down Expand Up @@ -1282,6 +1295,7 @@ final class MacAppState: ObservableObject {
self.translationOutputMode = preferences.translationOutputMode
self.visibleMenuBarLanguages = preferences.visibleMenuBarLanguages
self.performanceProfile = preferences.performanceProfile
self.autoResolveVoiceModelConflicts = preferences.autoResolveVoiceModelConflicts
self.selectedVoiceProviderID = preferences.selectedVoiceProviderID
self.selectedVoiceModelID = preferences.selectedVoiceModelID
self.voiceLanguageOverrides = preferences.voiceLanguageOverrides
Expand Down
11 changes: 6 additions & 5 deletions apps/macos/AppShell/MacAppStateFacadeActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ extension MacAppState {
}

func toggleDictationModeFromShortcut() {
streamingEnabled.toggle()
guard streamingEnabled else {
requestVoiceCapability(.liveText)
return
}
streamingEnabled = false
appendAudit("mode.toggle streamingEnabled=\(streamingEnabled)")
appendDiagnostic(
streamingEnabled
? "Live-Einfügen aktiviert. Der Wechsel gilt ab dem nächsten Diktat."
: "Live-Einfügen deaktiviert. Der Wechsel gilt ab dem nächsten Diktat.")
appendDiagnostic("Live-Einfügen deaktiviert. Der Wechsel gilt ab dem nächsten Diktat.")
}

func revealAppDataFolder() {
Expand Down
Loading
Loading