Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
10 changes: 8 additions & 2 deletions Sources/Fluid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1916,10 +1916,17 @@ struct ContentView: View {
if typingTarget.shouldRestoreOriginalFocus {
await self.restoreFocusToRecordingTarget()
}
if let element = AutoLearnDictionaryService.shared.captureFocusedElement() {
AutoLearnDictionaryService.shared.prepareMonitoring(pastedText: finalText, element: element)
Comment thread
martin-forge marked this conversation as resolved.
Outdated
}
self.asr.typeTextToActiveField(
finalText,
preferredTargetPID: typingTarget.pid
)
) {
// Now that typing is physically complete, we can start monitoring.
// This ensures we snapshot the fresh text instead of the pre-insertion empty state.
AutoLearnDictionaryService.shared.beginMonitoring()
}
didTypeExternally = true
}

Expand All @@ -1943,7 +1950,6 @@ struct ContentView: View {
aiModel: modelInfo.model,
aiProvider: modelInfo.provider
)

NotchOverlayManager.shared.hide()
} else if shouldPersistOutputs,
SettingsStore.shared.copyTranscriptionToClipboard == false,
Expand Down
226 changes: 226 additions & 0 deletions Sources/Fluid/Persistence/BackupService.swift

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,8 @@ final class SettingsStore: ObservableObject {
pauseMediaDuringTranscription: self.pauseMediaDuringTranscription,
vocabularyBoostingEnabled: self.vocabularyBoostingEnabled,
customDictionaryEntries: self.customDictionaryEntries,
autoLearnCustomDictionaryEnabled: self.autoLearnCustomDictionaryEnabled,
autoLearnCustomDictionarySuggestions: self.autoLearnCustomDictionarySuggestions,
selectedDictationPromptID: self.selectedDictationPromptID,
dictationPromptOff: self.isDictationPromptOff,
selectedEditPromptID: self.selectedEditPromptID,
Expand Down Expand Up @@ -2255,6 +2257,8 @@ final class SettingsStore: ObservableObject {
self.pauseMediaDuringTranscription = payload.pauseMediaDuringTranscription
self.vocabularyBoostingEnabled = payload.vocabularyBoostingEnabled
self.customDictionaryEntries = payload.customDictionaryEntries
self.autoLearnCustomDictionaryEnabled = payload.autoLearnCustomDictionaryEnabled
self.autoLearnCustomDictionarySuggestions = payload.autoLearnCustomDictionarySuggestions

self.dictationPromptProfiles = promptProfiles
self.appPromptBindings = appPromptBindings
Expand Down Expand Up @@ -2853,6 +2857,36 @@ final class SettingsStore: ObservableObject {
}
}

enum AutoLearnSuggestionStatus: String, Codable, Hashable {
case pending
case dismissed
}

struct AutoLearnSuggestion: Codable, Identifiable, Hashable {
let id: UUID
var originalText: String
var replacement: String
var occurrences: Int
var lastObservedAt: Date
var status: AutoLearnSuggestionStatus

init(
id: UUID = UUID(),
originalText: String,
replacement: String,
occurrences: Int = 1,
lastObservedAt: Date = Date(),
status: AutoLearnSuggestionStatus = .pending
) {
self.id = id
self.originalText = originalText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
self.replacement = replacement.trimmingCharacters(in: .whitespacesAndNewlines)
self.occurrences = occurrences
self.lastObservedAt = lastObservedAt
self.status = status
}
}

var vocabularyBoostingEnabled: Bool {
get {
let value = self.defaults.object(forKey: Keys.vocabularyBoostingEnabled)
Expand Down Expand Up @@ -2883,6 +2917,34 @@ final class SettingsStore: ObservableObject {
}
}

var autoLearnCustomDictionaryEnabled: Bool {
get {
let value = self.defaults.object(forKey: Keys.autoLearnCustomDictionaryEnabled)
return value as? Bool ?? false
}
set {
objectWillChange.send()
self.defaults.set(newValue, forKey: Keys.autoLearnCustomDictionaryEnabled)
}
}

var autoLearnCustomDictionarySuggestions: [AutoLearnSuggestion] {
get {
guard let data = defaults.data(forKey: Keys.autoLearnCustomDictionarySuggestions),
let decoded = try? JSONDecoder().decode([AutoLearnSuggestion].self, from: data)
else {
return []
}
return decoded
}
set {
objectWillChange.send()
if let encoded = try? JSONEncoder().encode(newValue) {
self.defaults.set(encoded, forKey: Keys.autoLearnCustomDictionarySuggestions)
}
}
}

// MARK: - Speech Model (Unified ASR Model Selection)

/// Unified speech recognition model selection.
Expand Down Expand Up @@ -3569,6 +3631,8 @@ private extension SettingsStore {
// Custom Dictionary
static let customDictionaryEntries = "CustomDictionaryEntries"
static let vocabularyBoostingEnabled = "VocabularyBoostingEnabled"
static let autoLearnCustomDictionaryEnabled = "AutoLearnCustomDictionaryEnabled"
static let autoLearnCustomDictionarySuggestions = "AutoLearnCustomDictionarySuggestions"

// Transcription Provider (ASR)
static let selectedTranscriptionProvider = "SelectedTranscriptionProvider"
Expand Down
8 changes: 4 additions & 4 deletions Sources/Fluid/Services/ASRService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2528,12 +2528,12 @@ final class ASRService: ObservableObject {

private let typingService = TypingService() // Reuse instance to avoid conflicts

func typeTextToActiveField(_ text: String) {
self.typingService.typeTextInstantly(text)
func typeTextToActiveField(_ text: String, onComplete: (() -> Void)? = nil) {
self.typingService.typeTextInstantly(text, onComplete: onComplete)
}

func typeTextToActiveField(_ text: String, preferredTargetPID: pid_t?) {
self.typingService.typeTextInstantly(text, preferredTargetPID: preferredTargetPID)
func typeTextToActiveField(_ text: String, preferredTargetPID: pid_t?, onComplete: (() -> Void)? = nil) {
self.typingService.typeTextInstantly(text, preferredTargetPID: preferredTargetPID, onComplete: onComplete)
}

/// Removes filler sounds from transcribed text
Expand Down
Loading