@@ -53,6 +53,8 @@ public final class ArgmaxSDKCoordinator: ObservableObject {
5353 @Published public private( set) var whisperKitModelState : ModelState = . unloaded
5454 @Published public private( set) var speakerKitModelState : ModelState = . unloaded
5555 @Published public var modelDownloadFailed : Bool = false
56+ @Published public var availableModelNames : [ String ] = [ ]
57+ @Published public private( set) var whisperKitModelProgress : Float = 0.0
5658
5759 // MARK: - Argmax API objects
5860 public private( set) var whisperKit : WhisperKitPro ? {
@@ -74,6 +76,10 @@ public final class ArgmaxSDKCoordinator: ObservableObject {
7476 private var apiKey : String ? = nil
7577 private var cancellables = Set < AnyCancellable > ( )
7678
79+ // MARK: - Progress tracking properties
80+ private let specializationProgressRatio : Float = 0.7
81+ private var progressAnimationTask : Task < Void , Never > ?
82+
7783 public init (
7884 whisperKitConfig: WhisperKitProConfig = WhisperKitProConfig ( ) ,
7985 keyProvider: APIKeyProvider
@@ -92,6 +98,9 @@ public final class ArgmaxSDKCoordinator: ObservableObject {
9298 self ? . objectWillChange. send ( )
9399 }
94100 . store ( in: & cancellables)
101+
102+ // Setup progress tracking observers
103+ setupProgressTracking ( )
95104 }
96105
97106 /// Sets up the Argmax SDK with proper configuration and error handling
@@ -137,6 +146,11 @@ public final class ArgmaxSDKCoordinator: ObservableObject {
137146 }
138147
139148 await modelStore. updateAvailableModels ( from: targetRepositories, keyProvider: keyProvider)
149+
150+ await MainActor . run {
151+ availableModelNames = modelStore. availableModels. flatMap ( \. models) . map ( \. description)
152+ }
153+
140154 }
141155
142156 /// Downloads the CoreML bundle (if needed) and instantiates both WhisperKit and SpeakerKit.
@@ -326,4 +340,103 @@ public final class ArgmaxSDKCoordinator: ObservableObject {
326340 }
327341 return speakerKit
328342 }
343+
344+ // MARK: - Progress tracking methods
345+
346+ /// Sets up observers for model state and progress changes to update whisperKitModelProgress
347+ ///
348+ /// ## Progress Calculation
349+ ///
350+ /// The `whisperKitModelProgress` represents the overall model loading progress from 0.0 to 1.0:
351+ /// - **Unloaded/Unloading:** 0.0
352+ /// - **Downloading:** `modelStore.progress.fractionCompleted * 0.7` (download contributes 70% of total progress)
353+ /// - **Downloaded:** 0.7 (download complete, specialization pending)
354+ /// - **Prewarming:** Animated smoothly from 0.7 to 0.91 over 60 seconds, or shorter if prewarming finishes ealier
355+ /// - **Prewarmed/Loading:** 0.91 (specialization complete, final loading steps)
356+ /// - **Loaded:** 1.0 (fully ready for transcription)
357+ private func setupProgressTracking( ) {
358+ // Observe whisperKitModelState changes
359+ $whisperKitModelState
360+ . sink { [ weak self] newState in
361+ self ? . updateProgressForModelState ( newState)
362+ }
363+ . store ( in: & cancellables)
364+
365+ // Observe modelStore.progress changes
366+ modelStore. $progress
367+ . sink { [ weak self] newProgress in
368+ self ? . updateProgressForDownload ( newProgress)
369+ }
370+ . store ( in: & cancellables)
371+ }
372+
373+ /// Updates whisperKitModelProgress based on model state changes
374+ private func updateProgressForModelState( _ newState: ModelState ) {
375+ progressAnimationTask? . cancel ( )
376+ switch newState {
377+ case . unloaded:
378+ whisperKitModelProgress = 0.0
379+ case . downloading:
380+ // Progress will be handled by download progress polling
381+ if whisperKitModelProgress == 0.0 {
382+ whisperKitModelProgress = 0.0
383+ }
384+ case . downloaded:
385+ whisperKitModelProgress = specializationProgressRatio
386+ case . prewarming, . loading:
387+ let startProgress = specializationProgressRatio
388+ let targetProgress = specializationProgressRatio + ( 1.0 - specializationProgressRatio) * 0.9
389+ progressAnimationTask = Task { [ weak self] in
390+ await self ? . updateProgressSmoothly ( from: startProgress, to: targetProgress, over: 60 )
391+ }
392+ case . prewarmed:
393+ whisperKitModelProgress = specializationProgressRatio + ( 1.0 - specializationProgressRatio) * 0.9
394+ case . loaded:
395+ whisperKitModelProgress = 1.0
396+ case . unloading:
397+ whisperKitModelProgress = 0.0
398+ @unknown default :
399+ break
400+ }
401+ }
402+
403+ /// Updates whisperKitModelProgress based on download progress changes
404+ private func updateProgressForDownload( _ newProgress: Progress ? ) {
405+ if let progress = newProgress, whisperKitModelState == . downloading {
406+ // Directly update progress from the Progress object
407+ let newProgressValue = Float ( progress. fractionCompleted) * specializationProgressRatio
408+ whisperKitModelProgress = newProgressValue
409+ } else if newProgress == nil && whisperKitModelState == . downloading {
410+ // Download completed, progress will be handled by modelState change
411+ whisperKitModelProgress = specializationProgressRatio
412+ }
413+ }
414+
415+ /// Smoothly animates progress from one value to another over a specified duration
416+ private func updateProgressSmoothly( from startValue: Float , to endValue: Float , over duration: TimeInterval ) async {
417+ let startTime = Date ( )
418+
419+ while true {
420+ let elapsedTime = Date ( ) . timeIntervalSince ( startTime)
421+
422+ if elapsedTime >= duration {
423+ await MainActor . run { [ weak self] in
424+ self ? . whisperKitModelProgress = endValue
425+ }
426+ break
427+ }
428+
429+ let percentage = Float ( elapsedTime / duration)
430+
431+ await MainActor . run { [ weak self] in
432+ self ? . whisperKitModelProgress = startValue + ( endValue - startValue) * percentage
433+ }
434+
435+ do {
436+ try await Task . sleep ( nanoseconds: 50_000_000 ) // 20fps
437+ } catch {
438+ break // Task was cancelled
439+ }
440+ }
441+ }
329442}
0 commit comments