Skip to content

Commit b11179a

Browse files
authored
Merge pull request #5 from argmaxinc/live-activity
2 parents b4270c5 + 3fd4342 commit b11179a

20 files changed

+1195
-221
lines changed

Playground.xcodeproj/project.pbxproj

Lines changed: 207 additions & 11 deletions
Large diffs are not rendered by default.

Playground/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>NSSupportsLiveActivities</key>
6+
<true/>
7+
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
8+
<true/>
59
<key>NSAudioCaptureUsageDescription</key>
610
<string>Required to record audio from other apps for transcription.</string>
711
<key>NSPrivacyAccessedAPITypes</key>

Playground/Playground.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ struct Playground: App {
7676
)
7777
self._audioProcessDiscoverer = StateObject(wrappedValue: processDiscoverer)
7878
#else
79+
let liveActivityMgr = LiveActivityManager()
7980
let streamVM = StreamViewModel(
8081
sdkCoordinator: coordinator,
81-
audioDeviceDiscoverer: deviceDiscoverer
82+
audioDeviceDiscoverer: deviceDiscoverer,
83+
liveActivityManager: liveActivityMgr
8284
)
8385
#endif
8486
let transcribeVM = TranscribeViewModel(sdkCoordinator: coordinator)
@@ -102,6 +104,12 @@ struct Playground: App {
102104
.onAppear {
103105
sdkCoordinator.setupArgmax()
104106
analyticsLogger.configureIfNeeded()
107+
#if os(iOS)
108+
// Clean up lingering activities, this might happen if app is in background for a longtime and put to sleep by system
109+
Task {
110+
await streamViewModel.liveActivityManager.cleanupOrphanedActivities()
111+
}
112+
#endif
105113
}
106114
#if os(macOS)
107115
.frame(minWidth: 1000, minHeight: 700)

Playground/Services/ArgmaxSDKCoordinator.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)