From d09a9fba7f506145d713e2656522426b24b30aa5 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Fri, 1 Aug 2025 17:26:08 +0545 Subject: [PATCH] Experimental new models --- VoiceInk.xcodeproj/project.pbxproj | 27 ++- .../xcshareddata/swiftpm/Package.resolved | 11 +- VoiceInk/Models/PredefinedModels.swift | 6 +- VoiceInk/Models/TranscriptionModel.swift | 18 ++ VoiceInk/PowerMode/ActiveWindowService.swift | 16 +- VoiceInk/Recorder.swift | 7 + .../Sounds/{pastes.mp3 => pastess.mp3} | Bin VoiceInk/Resources/Sounds/recstart.mp3 | Bin 2315 -> 14740 bytes VoiceInk/Resources/Sounds/recstop.mp3 | Bin 0 -> 14333 bytes .../AudioFileTranscriptionManager.swift | 8 + .../AudioFileTranscriptionService.swift | 3 + .../ParakeetTranscriptionService.swift | 89 +++++++++ VoiceInk/SoundManager.swift | 6 +- .../Views/AI Models/ModelCardRowView.swift | 8 + .../Views/AI Models/ModelManagementView.swift | 3 +- .../AI Models/ParakeetModelCardRowView.swift | 173 ++++++++++++++++++ .../Whisper/WhisperState+ModelQueries.swift | 2 + VoiceInk/Whisper/WhisperState+Parakeet.swift | 82 +++++++++ VoiceInk/Whisper/WhisperState.swift | 12 +- 19 files changed, 448 insertions(+), 23 deletions(-) rename VoiceInk/Resources/Sounds/{pastes.mp3 => pastess.mp3} (100%) create mode 100755 VoiceInk/Resources/Sounds/recstop.mp3 create mode 100644 VoiceInk/Services/ParakeetTranscriptionService.swift create mode 100644 VoiceInk/Views/AI Models/ParakeetModelCardRowView.swift create mode 100644 VoiceInk/Whisper/WhisperState+Parakeet.swift diff --git a/VoiceInk.xcodeproj/project.pbxproj b/VoiceInk.xcodeproj/project.pbxproj index 7c60bb8..356f30a 100644 --- a/VoiceInk.xcodeproj/project.pbxproj +++ b/VoiceInk.xcodeproj/project.pbxproj @@ -7,13 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + E1304F742E3B9E8A0001F9E2 /* FluidAudio in Frameworks */ = {isa = PBXBuildFile; productRef = E1304F732E3B9E8A0001F9E2 /* FluidAudio */; }; + E1304F842E3BB2FF0001F9E2 /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1304F832E3BB2FF0001F9E2 /* whisper.xcframework */; }; + E1304F852E3BB2FF0001F9E2 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E1304F832E3BB2FF0001F9E2 /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = E1A261112CC143AC00B233D1 /* KeyboardShortcuts */; }; - E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */; }; E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; }; E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD45E2CC544F100303ECB /* Sparkle */; }; E1D7EF992E35E16C00640029 /* MediaRemoteAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */; }; E1D7EF9A2E35E19B00640029 /* MediaRemoteAdapter in Embed Frameworks */ = {isa = PBXBuildFile; productRef = E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - E1E0B9622E3133EF00C10E20 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1F5FA7A2DA6CBF900B1FD8A /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = E1F5FA792DA6CBF900B1FD8A /* Zip */; }; /* End PBXBuildFile section */ @@ -41,7 +42,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - E1E0B9622E3133EF00C10E20 /* whisper.xcframework in Embed Frameworks */, + E1304F852E3BB2FF0001F9E2 /* whisper.xcframework in Embed Frameworks */, E1D7EF9A2E35E19B00640029 /* MediaRemoteAdapter in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -53,6 +54,7 @@ E11473B02CBE0F0A00318EE4 /* VoiceInk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoiceInk.app; sourceTree = BUILT_PRODUCTS_DIR; }; E11473C32CBE0F0B00318EE4 /* VoiceInkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E11473CD2CBE0F0B00318EE4 /* VoiceInkUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E1304F832E3BB2FF0001F9E2 /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../Downloads/build-apple/whisper.xcframework"; sourceTree = ""; }; E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -81,9 +83,10 @@ files = ( E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */, E1D7EF992E35E16C00640029 /* MediaRemoteAdapter in Frameworks */, + E1304F742E3B9E8A0001F9E2 /* FluidAudio in Frameworks */, + E1304F842E3BB2FF0001F9E2 /* whisper.xcframework in Frameworks */, E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */, E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */, - E1A8C8CB2E1257B7003E58EC /* whisper.xcframework in Frameworks */, E1F5FA7A2DA6CBF900B1FD8A /* Zip in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -129,6 +132,7 @@ E114741C2CBE1DE200318EE4 /* Frameworks */ = { isa = PBXGroup; children = ( + E1304F832E3BB2FF0001F9E2 /* whisper.xcframework */, E1A8C8CA2E1257B7003E58EC /* whisper.xcframework */, ); name = Frameworks; @@ -160,6 +164,7 @@ E1ADD45E2CC544F100303ECB /* Sparkle */, E1F5FA792DA6CBF900B1FD8A /* Zip */, E1D7EF982E35E16C00640029 /* MediaRemoteAdapter */, + E1304F732E3B9E8A0001F9E2 /* FluidAudio */, ); productName = VoiceInk; productReference = E11473B02CBE0F0A00318EE4 /* VoiceInk.app */; @@ -249,6 +254,7 @@ E1ADD45D2CC544F100303ECB /* XCRemoteSwiftPackageReference "Sparkle" */, E1F5FA782DA6CBF900B1FD8A /* XCRemoteSwiftPackageReference "Zip" */, E1D7EF972E35E16C00640029 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */, + E1304F722E3B9E8A0001F9E2 /* XCRemoteSwiftPackageReference "FluidAudio" */, ); preferredProjectObjectVersion = 77; productRefGroup = E11473B12CBE0F0A00318EE4 /* Products */; @@ -618,6 +624,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + E1304F722E3B9E8A0001F9E2 /* XCRemoteSwiftPackageReference "FluidAudio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/FluidInference/FluidAudio"; + requirement = { + branch = main; + kind = branch; + }; + }; E1A261102CC143AC00B233D1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; @@ -661,6 +675,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + E1304F732E3B9E8A0001F9E2 /* FluidAudio */ = { + isa = XCSwiftPackageProductDependency; + package = E1304F722E3B9E8A0001F9E2 /* XCRemoteSwiftPackageReference "FluidAudio" */; + productName = FluidAudio; + }; E1A261112CC143AC00B233D1 /* KeyboardShortcuts */ = { isa = XCSwiftPackageProductDependency; package = E1A261102CC143AC00B233D1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; diff --git a/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 713844b..b0ebaa8 100644 --- a/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/VoiceInk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "ef9c2994fdcb030d4d27f817e99251821e662f56f62355a728a019e924262633", + "originHash" : "b78069b2535604c42957e4e3be638514547280f6779f44a2b633aab9602881d9", "pins" : [ + { + "identity" : "fluidaudio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidInference/FluidAudio", + "state" : { + "branch" : "main", + "revision" : "2de87c32c320e2f28839c3a9682bc7bd0ea45be7" + } + }, { "identity" : "keyboardshortcuts", "kind" : "remoteSourceControl", diff --git a/VoiceInk/Models/PredefinedModels.swift b/VoiceInk/Models/PredefinedModels.swift index 6bf4e76..d3c80aa 100644 --- a/VoiceInk/Models/PredefinedModels.swift +++ b/VoiceInk/Models/PredefinedModels.swift @@ -87,8 +87,8 @@ import Foundation supportedLanguages: getLanguageDictionary(isMultilingual: true, provider: .nativeApple) ), - // Fluid Audio Model - FluidAudioModel( + // Parakeet Model + ParakeetModel( name: "parakeet-tdt-0.6b", displayName: "Parakeet", description: "NVIDIA's insanely fast Parakeet model for lightning-fast transcription.", @@ -96,7 +96,7 @@ import Foundation speed: 0.99, accuracy: 0.94, ramUsage: 0.8, - supportedLanguages: getLanguageDictionary(isMultilingual: false, provider: .fluidAudio) + supportedLanguages: getLanguageDictionary(isMultilingual: false, provider: .parakeet) ), // Local Models diff --git a/VoiceInk/Models/TranscriptionModel.swift b/VoiceInk/Models/TranscriptionModel.swift index 5515531..4ea95b3 100644 --- a/VoiceInk/Models/TranscriptionModel.swift +++ b/VoiceInk/Models/TranscriptionModel.swift @@ -3,6 +3,7 @@ import Foundation // Enum to differentiate between model providers enum ModelProvider: String, Codable, Hashable, CaseIterable { case local = "Local" + case parakeet = "Parakeet" case groq = "Groq" case elevenLabs = "ElevenLabs" case deepgram = "Deepgram" @@ -46,6 +47,23 @@ struct NativeAppleModel: TranscriptionModel { let supportedLanguages: [String: String] } +// A new struct for Parakeet models +struct ParakeetModel: TranscriptionModel { + let id = UUID() + let name: String + let displayName: String + let description: String + let provider: ModelProvider = .parakeet + let size: String + let speed: Double + let accuracy: Double + let ramUsage: Double + var isMultilingualModel: Bool { + supportedLanguages.count > 1 + } + let supportedLanguages: [String: String] +} + // A new struct for cloud models struct CloudModel: TranscriptionModel { let id: UUID diff --git a/VoiceInk/PowerMode/ActiveWindowService.swift b/VoiceInk/PowerMode/ActiveWindowService.swift index 3388ac2..5d05139 100644 --- a/VoiceInk/PowerMode/ActiveWindowService.swift +++ b/VoiceInk/PowerMode/ActiveWindowService.swift @@ -126,25 +126,23 @@ class ActiveWindowService: ObservableObject { // Set the new model as default. This works for both local and cloud models. await whisperState.setDefaultTranscriptionModel(selectedModel) - // The cleanup and load cycle is only necessary for local models. - if selectedModel.provider == ModelProvider.local { - // Unload any previously loaded model to free up memory. + switch selectedModel.provider { + case .local: await whisperState.cleanupModelResources() - // Load the new local model into memory. if let localModel = await whisperState.availableModels.first(where: { $0.name == selectedModel.name }) { do { try await whisperState.loadModel(localModel) - logger.info("✅ Power Mode: Successfully loaded local model '\(localModel.name)'.") } catch { logger.error("❌ Power Mode: Failed to load local model '\(localModel.name)': \(error.localizedDescription)") } } - } else { - // For cloud models, no in-memory loading is needed, but we should still - // clean up if the *previous* model was a local one. + + case .parakeet: + await whisperState.cleanupModelResources() + + default: await whisperState.cleanupModelResources() - logger.info("✅ Power Mode: Switched to cloud model '\(selectedModel.name)'. No local load needed.") } } } diff --git a/VoiceInk/Recorder.swift b/VoiceInk/Recorder.swift index f6a7104..12937ee 100644 --- a/VoiceInk/Recorder.swift +++ b/VoiceInk/Recorder.swift @@ -150,9 +150,16 @@ class Recorder: ObservableObject { recorder?.stop() recorder = nil audioMeter = AudioMeter(averagePower: 0, peakPower: 0) + Task { + // Complete system audio operations first await mediaController.unmuteSystemAudio() await playbackController.resumeMedia() + + // Then play stop sound on main thread after audio operations are complete + await MainActor.run { + SoundManager.shared.playStopSound() + } } deviceManager.isRecordingActive = false } diff --git a/VoiceInk/Resources/Sounds/pastes.mp3 b/VoiceInk/Resources/Sounds/pastess.mp3 similarity index 100% rename from VoiceInk/Resources/Sounds/pastes.mp3 rename to VoiceInk/Resources/Sounds/pastess.mp3 diff --git a/VoiceInk/Resources/Sounds/recstart.mp3 b/VoiceInk/Resources/Sounds/recstart.mp3 index be15dac9c33434ec38a45e11a137807d84ff78fc..fe3e8b2549fc45f9a724a7b0a851f9b72539f422 100755 GIT binary patch literal 14740 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�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 DQmI$v literal 2315 zcmcJR`9IX#AICqKvCND$Vu&Oz#x@LDvs^=HWX~2i3E7f0Dhx$4_Kdx&5SN53!-v<7a3hBhwxDbuTaq zM3+<>sppmC*{Y(xN)L_Y?bdI*S#*QR2gr{jZJ)$v%g5ICBO=wfl}NOImcn%0RUf?+ z%9Ac?v9BW{yZ7z&4f6emPrZ5VD0Shfq!3r}eQ?*BdJ@kgi?natz%yMd^k)&n0~B<7 z&b^7f%#@nx_wf-*xP6Zf@}TI^&1viAy0nfSx9r)E48f?s{%w={a6n6T@=xtC1t(LCBB2F?rKP=t@L9rhRvEdP_=JtE*08?Ow+`VNQf1W;2AThsCK7>2!EN)AXZOD_d=HWM8ue% z#nxe6`EcgxLo^gLJOq_f%Y@nZ|>@C`dh$5vvuv1~6fGXl(_A$wLBHk_9>I|>*C zZ70{Sv@Zgv<-*A>6<7vy{PIEC4JLqT!%sG*Qbg2qjqrIS+b6QeMLx(HM_^(uaP*d- znyS8*sgpD}XsRBqo`^&zx?5y?_0-0=zO*f6)V`3@#(8`g%WI*MENQ2GVkpXkLmI^S z@Q!Cq%ew~&On3kSJ87DrIpA?51jWP4nw8Wt0H&zyMRhSeGdvPfDP-iegnn8Q7v`oc zv| z>wb?HJLBO?E||U{K}}!`lThx2nXMMrI>Cv~v-{ewhkO^#=B@u(-R$?y&iiCPtKI>3 zb{h!3R?@ubeErpANR?)N!%raqZZhcAm;vw-#eEFXbS6{rYQQn`lr4$qq?$ILlwUc* z@<4n>hJb?OWle@SyPZU;-@1}{?+nWz)cYX8t=!O#QutS(+xD3Z+09sS@1kXb6I+>P%ysEFi6MtfF=J!K70plE z8YGOQLsY504*#3N^hu1J z?F-p1E**gK-G+W|Iue55;S~dhE)M`E@9Zx~a*BATOr`Q@jr1MSfK;8eL~iY7`4S#| zJc?3%FXHjJI&67hu$7-pg|ksHeQ)r)g|fNcx3RklIBSxPJx{&U6ulMQo}uYEbAtbr zcU^m>w@yRWx%!~j002kQ31rX3ju@LhTI4vth+zB^^-%>_ynQUgr95J~bv;H+I5T%l zHwb%eG=<{MP!ZrOO`3RCT+@(L>(I0zjBKYb^wrmOAg4oPjiB6nsgrA7WS~UBVyD$V zM=UvwOTgpI;;%ad(|CSCI>#cyg5Pr^9uet6r^V?GmXiBbgZ28MJp2fR+1d|ro8r;7 z=H0gvowg$B<1~(P1@rCf>$fh-vdxvP-p6Wv*Tej=tG&&WtEgiOj>XKw#UE%`CznB3 zB{(Ki(~>Z}&MJJ)Vh`o7Jeq|MScJ*^qswiV(HmR%kne$)z9BI%vbO<+?!;bLoK*Zq zN)CjEPnjVFV#9SAv*ppP-tX5xY`c`O126Q)jJVvbOCacR2L&5N?|z%wHT9M64Sfi2 zvn!V!Z-^cV@y5VRLM40sD3sqxpFK!08RDDT|J3ATGC7HU+A_Slfdk69zv0t4b((~l zr7eqVO7IFWrr4qmr{Db5|B}DEgLmU9K>B(Bf2#|twxjI1K7Y_%nq?XETHyC#dSu^P zCk0a*H`TUEPdq=na_!=}-d9Xq&vtIa3nyDw4Lz-)?g%u!$fQNF1^)9T-6=x@S=PLX zCNZ9RcuCf6KOUjfnKf5n%ZSIn`mF7o&W@*`bw~W;ydcdQxf8kO-_XP1`KUn7@0#d9 z<@L`;LSg{NYKiQ_YWD?EqJ%*_VtOhoY*KBz^^cDM1Bk8kd^&mWLJ22E6avf%j#yTC zd5rUytZ=?~-9B!>X`c#}KApd-rN;E4Ao`@h< YIu5_PTs5%6i!(4c!W{ib?EX*u7pA)@egFUf diff --git a/VoiceInk/Resources/Sounds/recstop.mp3 b/VoiceInk/Resources/Sounds/recstop.mp3 new file mode 100755 index 0000000000000000000000000000000000000000..d5d4f76fbd2823e3a9afa4ee58488c4f229fac1d GIT binary patch literal 14333 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 String { + do { + defer { + asrManager?.cleanup() + self.asrManager = nil + self.isModelLoaded = false + } + + if !isModelLoaded { + try await loadModel() + } + + guard let asrManager = asrManager else { + throw NSError(domain: "ParakeetTranscriptionService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize ASR manager."]) + } + + let audioSamples = try readAudioSamples(from: audioURL) + let result = try await asrManager.transcribe(audioSamples) + + if UserDefaults.standard.object(forKey: "IsTextFormattingEnabled") as? Bool ?? true { + return WhisperTextFormatter.format(result.text) + } + return result.text + } catch { + let errorMessage = error.localizedDescription + await MainActor.run { + NotificationManager.shared.showNotification( + title: "Transcription Failed: \(errorMessage)", + type: .error + ) + } + return "" + } + } + + private func readAudioSamples(from url: URL) throws -> [Float] { + let data = try Data(contentsOf: url) + // A basic check, assuming a more robust check happens elsewhere. + guard data.count > 44 else { return [] } + + let floats = stride(from: 44, to: data.count, by: 2).map { + return data[$0..<$0 + 2].withUnsafeBytes { + let short = Int16(littleEndian: $0.load(as: Int16.self)) + return max(-1.0, min(Float(short) / 32767.0, 1.0)) + } + } + return floats + } +} \ No newline at end of file diff --git a/VoiceInk/SoundManager.swift b/VoiceInk/SoundManager.swift index f0768d9..9b20dd9 100644 --- a/VoiceInk/SoundManager.swift +++ b/VoiceInk/SoundManager.swift @@ -20,7 +20,7 @@ class SoundManager { // Try loading directly from the main bundle if let startSoundURL = Bundle.main.url(forResource: "recstart", withExtension: "mp3"), - let stopSoundURL = Bundle.main.url(forResource: "pastes", withExtension: "mp3"), + let stopSoundURL = Bundle.main.url(forResource: "recstop", withExtension: "mp3"), let escSoundURL = Bundle.main.url(forResource: "esc", withExtension: "wav") { print("Found sounds in main bundle") try? loadSounds(start: startSoundURL, stop: stopSoundURL, esc: escSoundURL) @@ -49,8 +49,8 @@ class SoundManager { escSound = try AVAudioPlayer(contentsOf: escURL) // Set lower volume for all sounds - startSound?.volume = 0.7 - stopSound?.volume = 0.7 + startSound?.volume = 0.4 + stopSound?.volume = 0.4 escSound?.volume = 0.3 // Prepare sounds for instant playback diff --git a/VoiceInk/Views/AI Models/ModelCardRowView.swift b/VoiceInk/Views/AI Models/ModelCardRowView.swift index 8a3c7c1..efa4ff8 100644 --- a/VoiceInk/Views/AI Models/ModelCardRowView.swift +++ b/VoiceInk/Views/AI Models/ModelCardRowView.swift @@ -3,6 +3,7 @@ import AppKit struct ModelCardRowView: View { let model: any TranscriptionModel + @ObservedObject var whisperState: WhisperState let isDownloaded: Bool let isCurrent: Bool let downloadProgress: [String: Double] @@ -30,6 +31,13 @@ struct ModelCardRowView: View { downloadAction: downloadAction ) } + case .parakeet: + if let parakeetModel = model as? ParakeetModel { + ParakeetModelCardRowView( + model: parakeetModel, + whisperState: whisperState + ) + } case .nativeApple: if let nativeAppleModel = model as? NativeAppleModel { NativeAppleModelCardView( diff --git a/VoiceInk/Views/AI Models/ModelManagementView.swift b/VoiceInk/Views/AI Models/ModelManagementView.swift index 25c4db2..387b1b0 100644 --- a/VoiceInk/Views/AI Models/ModelManagementView.swift +++ b/VoiceInk/Views/AI Models/ModelManagementView.swift @@ -118,6 +118,7 @@ struct ModelManagementView: View { ForEach(filteredModels, id: \.id) { model in ModelCardRowView( model: model, + whisperState: whisperState, isDownloaded: whisperState.availableModels.contains { $0.name == model.name }, isCurrent: whisperState.currentTranscriptionModel?.name == model.name, downloadProgress: whisperState.downloadProgress, @@ -190,7 +191,7 @@ struct ModelManagementView: View { return index1 < index2 } case .local: - return whisperState.allAvailableModels.filter { $0.provider == .local || $0.provider == .nativeApple } + return whisperState.allAvailableModels.filter { $0.provider == .local || $0.provider == .nativeApple || $0.provider == .parakeet } case .cloud: let cloudProviders: [ModelProvider] = [.groq, .elevenLabs, .deepgram, .mistral] return whisperState.allAvailableModels.filter { cloudProviders.contains($0.provider) } diff --git a/VoiceInk/Views/AI Models/ParakeetModelCardRowView.swift b/VoiceInk/Views/AI Models/ParakeetModelCardRowView.swift new file mode 100644 index 0000000..695b49d --- /dev/null +++ b/VoiceInk/Views/AI Models/ParakeetModelCardRowView.swift @@ -0,0 +1,173 @@ +import SwiftUI +import Combine +import AppKit + +struct ParakeetModelCardRowView: View { + let model: ParakeetModel + @ObservedObject var whisperState: WhisperState + + var isCurrent: Bool { + whisperState.currentTranscriptionModel?.name == model.name + } + + var isDownloaded: Bool { + whisperState.isParakeetModelDownloaded + } + + var isDownloading: Bool { + whisperState.isDownloadingParakeet + } + + var body: some View { + HStack(alignment: .top, spacing: 16) { + VStack(alignment: .leading, spacing: 6) { + headerSection + metadataSection + descriptionSection + progressSection + } + .frame(maxWidth: .infinity, alignment: .leading) + + actionSection + } + .padding(16) + .background(CardBackground(isSelected: isCurrent, useAccentGradientWhenSelected: isCurrent)) + } + + private var headerSection: some View { + HStack(alignment: .firstTextBaseline) { + Text(model.displayName) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(Color(.labelColor)) + + Text("Experimental") + .font(.system(size: 11, weight: .medium)) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Capsule().fill(Color.orange.opacity(0.8))) + .foregroundColor(.white) + + statusBadge + Spacer() + } + } + + private var statusBadge: some View { + Group { + if isCurrent { + Text("Default") + .font(.system(size: 11, weight: .medium)) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Capsule().fill(Color.accentColor)) + .foregroundColor(.white) + } else if isDownloaded { + Text("Downloaded") + .font(.system(size: 11, weight: .medium)) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Capsule().fill(Color(.quaternaryLabelColor))) + .foregroundColor(Color(.labelColor)) + } + } + } + + private var metadataSection: some View { + HStack(spacing: 12) { + Label(model.language, systemImage: "globe") + Label(model.size, systemImage: "internaldrive") + HStack(spacing: 3) { + Text("Speed") + progressDotsWithNumber(value: model.speed * 10) + } + HStack(spacing: 3) { + Text("Accuracy") + progressDotsWithNumber(value: model.accuracy * 10) + } + } + .font(.system(size: 11)) + .foregroundColor(Color(.secondaryLabelColor)) + .lineLimit(1) + } + + private var descriptionSection: some View { + Text(model.description) + .font(.system(size: 11)) + .foregroundColor(Color(.secondaryLabelColor)) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 4) + } + + private var progressSection: some View { + Group { + if isDownloading { + ProgressView() // Indeterminate for now + .progressViewStyle(LinearProgressViewStyle()) + .frame(maxWidth: 200) + .padding(.top, 8) + } + } + } + + private var actionSection: some View { + HStack(spacing: 8) { + if isCurrent { + Text("Default Model") + .font(.system(size: 12)) + .foregroundColor(Color(.secondaryLabelColor)) + } else if isDownloaded { + Button(action: { + Task { + await whisperState.setDefaultTranscriptionModel(model) + } + }) { + Text("Set as Default") + .font(.system(size: 12)) + } + .buttonStyle(.bordered) + .controlSize(.small) + } else { + Button(action: { + Task { + await whisperState.downloadParakeetModel() + } + }) { + HStack(spacing: 4) { + Text(isDownloading ? "Downloading..." : "Download") + Image(systemName: "arrow.down.circle") + } + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Capsule().fill(Color.accentColor)) + } + .buttonStyle(.plain) + .disabled(isDownloading) + } + + if isDownloaded { + Menu { + Button(action: { + whisperState.deleteParakeetModel() + }) { + Label("Delete Model", systemImage: "trash") + } + + Button { + whisperState.showParakeetModelInFinder() + } label: { + Label("Show in Finder", systemImage: "folder") + } + } label: { + Image(systemName: "ellipsis.circle") + .font(.system(size: 14)) + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .frame(width: 20, height: 20) + } + } + } +} diff --git a/VoiceInk/Whisper/WhisperState+ModelQueries.swift b/VoiceInk/Whisper/WhisperState+ModelQueries.swift index 63bf707..f38d39a 100644 --- a/VoiceInk/Whisper/WhisperState+ModelQueries.swift +++ b/VoiceInk/Whisper/WhisperState+ModelQueries.swift @@ -6,6 +6,8 @@ extension WhisperState { switch model.provider { case .local: return availableModels.contains { $0.name == model.name } + case .parakeet: + return isParakeetModelDownloaded case .nativeApple: if #available(macOS 26, *) { return true diff --git a/VoiceInk/Whisper/WhisperState+Parakeet.swift b/VoiceInk/Whisper/WhisperState+Parakeet.swift new file mode 100644 index 0000000..9746a49 --- /dev/null +++ b/VoiceInk/Whisper/WhisperState+Parakeet.swift @@ -0,0 +1,82 @@ +import Foundation +import FluidAudio +import AppKit + +extension WhisperState { + var isParakeetModelDownloaded: Bool { + get { UserDefaults.standard.bool(forKey: "ParakeetModelDownloaded") } + set { UserDefaults.standard.set(newValue, forKey: "ParakeetModelDownloaded") } + } + + var isParakeetModelDownloading: Bool { + get { isDownloadingParakeet } + set { isDownloadingParakeet = newValue } + } + + @MainActor + func downloadParakeetModel() async { + if isParakeetModelDownloaded { + return + } + + isDownloadingParakeet = true + downloadProgress["parakeet-tdt-0.6b"] = 0.0 + + do { + _ = try await AsrModels.downloadAndLoad(to: parakeetModelsDirectory) + self.isParakeetModelDownloaded = true + } catch { + self.isParakeetModelDownloaded = false + } + + isDownloadingParakeet = false + downloadProgress["parakeet-tdt-0.6b"] = nil + + refreshAllAvailableModels() + } + + @MainActor + func deleteParakeetModel() { + if let currentModel = currentTranscriptionModel, currentModel.provider == .parakeet { + currentTranscriptionModel = nil + UserDefaults.standard.removeObject(forKey: "CurrentTranscriptionModel") + } + + do { + // First try: app support directory + bundle path + let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("com.prakashjoshipax.VoiceInk") + let parakeetModelDirectory = appSupportDirectory.appendingPathComponent("parakeet-tdt-0.6b-v2-coreml") + + if FileManager.default.fileExists(atPath: parakeetModelDirectory.path) { + try FileManager.default.removeItem(at: parakeetModelDirectory) + } else { + // Second try: root of application support directory + let rootAppSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + let rootParakeetModelDirectory = rootAppSupportDirectory.appendingPathComponent("parakeet-tdt-0.6b-v2-coreml") + + if FileManager.default.fileExists(atPath: rootParakeetModelDirectory.path) { + try FileManager.default.removeItem(at: rootParakeetModelDirectory) + } + } + + self.isParakeetModelDownloaded = false + + } catch { + // Silently fail + } + + refreshAllAvailableModels() + } + + @MainActor + func showParakeetModelInFinder() { + let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("com.prakashjoshipax.VoiceInk") + let parakeetModelDirectory = appSupportDirectory.appendingPathComponent("parakeet-tdt-0.6b-v2-coreml") + + if FileManager.default.fileExists(atPath: parakeetModelDirectory.path) { + NSWorkspace.shared.selectFile(parakeetModelDirectory.path, inFileViewerRootedAtPath: "") + } + } +} diff --git a/VoiceInk/Whisper/WhisperState.swift b/VoiceInk/Whisper/WhisperState.swift index 5a2f387..6b4b6c5 100644 --- a/VoiceInk/Whisper/WhisperState.swift +++ b/VoiceInk/Whisper/WhisperState.swift @@ -62,6 +62,7 @@ class WhisperState: NSObject, ObservableObject { private var localTranscriptionService: LocalTranscriptionService! private lazy var cloudTranscriptionService = CloudTranscriptionService() private lazy var nativeAppleTranscriptionService = NativeAppleTranscriptionService() + private lazy var parakeetTranscriptionService = ParakeetTranscriptionService(customModelsDirectory: parakeetModelsDirectory) private var modelUrl: URL? { let possibleURLs = [ @@ -84,6 +85,7 @@ class WhisperState: NSObject, ObservableObject { let modelsDirectory: URL let recordingsDirectory: URL + let parakeetModelsDirectory: URL let enhancementService: AIEnhancementService? var licenseViewModel: LicenseViewModel let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "WhisperState") @@ -92,6 +94,7 @@ class WhisperState: NSObject, ObservableObject { // For model progress tracking @Published var downloadProgress: [String: Double] = [:] + @Published var isDownloadingParakeet = false init(modelContext: ModelContext, enhancementService: AIEnhancementService? = nil) { self.modelContext = modelContext @@ -100,6 +103,7 @@ class WhisperState: NSObject, ObservableObject { self.modelsDirectory = appSupportDirectory.appendingPathComponent("WhisperModels") self.recordingsDirectory = appSupportDirectory.appendingPathComponent("Recordings") + self.parakeetModelsDirectory = appSupportDirectory.appendingPathComponent("ParakeetModels") self.enhancementService = enhancementService self.licenseViewModel = LicenseViewModel() @@ -167,10 +171,11 @@ class WhisperState: NSObject, ObservableObject { await MainActor.run { self.recordingState = .recording + SoundManager.shared.playStartSound() } await ActiveWindowService.shared.applyConfigurationForCurrentApp() - + // Only load model if it's a local model and not already loaded if let model = self.currentTranscriptionModel, model.provider == .local { if let localWhisperModel = self.availableModels.first(where: { $0.name == model.name }), @@ -181,6 +186,8 @@ class WhisperState: NSObject, ObservableObject { self.logger.error("❌ Model loading failed: \(error.localizedDescription)") } } + } else if let model = self.currentTranscriptionModel, model.provider == .parakeet { + try? await parakeetTranscriptionService.loadModel() } if let enhancementService = self.enhancementService, @@ -239,6 +246,8 @@ class WhisperState: NSObject, ObservableObject { switch model.provider { case .local: transcriptionService = localTranscriptionService + case .parakeet: + transcriptionService = parakeetTranscriptionService case .nativeApple: transcriptionService = nativeAppleTranscriptionService default: @@ -332,7 +341,6 @@ class WhisperState: NSObject, ObservableObject { if await checkCancellationAndCleanup() { return } - SoundManager.shared.playStopSound() DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { CursorPaster.pasteAtCursor(text, shouldPreserveClipboard: !self.isAutoCopyEnabled)