Skip to content

Commit 9c96d9d

Browse files
tts support (#962)
* initial TTS setup * minor improvements * fixed incorrect download state of tts model * fixed cell reuse bug which was causing action buttons to hide * added voice picker * added TTS as a tool * improved visual updates for tts tool call * fixed a bug in speaker button in message cell * stop audio playback at appropriate places
1 parent c7a8f08 commit 9c96d9d

18 files changed

Lines changed: 1266 additions & 13 deletions

Packages/OsaurusCore/AppDelegate.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ public final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelega
9090
// Configure local notifications
9191
NotificationService.shared.configureOnLaunch()
9292

93+
// If PocketTTS models are already on disk, preload them so the first
94+
// speaker tap plays immediately without routing to settings.
95+
TTSService.shared.refreshModelState()
96+
9397
// Set up observers for server state changes
9498
setupObservers()
9599

@@ -394,6 +398,14 @@ public final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelega
394398
object: nil
395399
)
396400

401+
// Route "user tapped speaker but model isn't ready" to the TTS settings tab.
402+
NotificationCenter.default.addObserver(
403+
self,
404+
selector: #selector(handleOpenTTSSettings(_:)),
405+
name: .openTTSSettingsRequested,
406+
object: nil
407+
)
408+
397409
// Listen for chat view closed to resume VAD
398410
NotificationCenter.default.addObserver(
399411
self,
@@ -487,6 +499,13 @@ public final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelega
487499
}
488500
}
489501

502+
@objc private func handleOpenTTSSettings(_ notification: Notification) {
503+
Task { @MainActor in
504+
ManagementStateManager.shared.voiceSubTabRequest = "TTS"
505+
showManagementWindow(initialTab: .voice)
506+
}
507+
}
508+
490509
public func application(_ application: NSApplication, open urls: [URL]) {
491510
for url in urls {
492511
handleDeepLink(url)

Packages/OsaurusCore/Folder/ChatExecutionContext.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ public enum ChatExecutionContext {
2424

2525
/// The agent ID whose context is active for the current execution.
2626
@TaskLocal public static var currentAgentId: UUID?
27+
28+
/// Assistant turn dispatching the current tool call. Used by `speak`
29+
/// to bind TTS playback to the right message bubble
30+
@TaskLocal public static var currentAssistantTurnId: UUID?
31+
32+
/// Specific tool invocation id. Used by `speak` so the inline card
33+
/// can swap its check for a spinner while its audio plays
34+
@TaskLocal public static var currentToolCallId: String?
2735
}

Packages/OsaurusCore/Managers/Chat/ChatWindowState.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ final class ChatWindowState: ObservableObject {
136136
}
137137

138138
func switchAgent(to newAgentId: UUID) {
139+
TTSService.shared.stop()
139140
if !session.turns.isEmpty { session.save() }
140141
agentId = newAgentId
141142
removeEphemeralProviderIfNeeded()
@@ -150,6 +151,7 @@ final class ChatWindowState: ObservableObject {
150151
}
151152

152153
func startNewChat() {
154+
TTSService.shared.stop()
153155
if !session.turns.isEmpty { session.save() }
154156
flushCurrentSession()
155157
session.reset(for: agentId)
@@ -158,6 +160,7 @@ final class ChatWindowState: ObservableObject {
158160

159161
func loadSession(_ sessionData: ChatSessionData) {
160162
guard sessionData.id != session.sessionId else { return }
163+
TTSService.shared.stop()
161164
if !session.turns.isEmpty { session.save() }
162165
flushCurrentSession()
163166

Packages/OsaurusCore/Managers/ManagementStateManager.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@ public final class ManagementStateManager: ObservableObject {
1616
/// Persists the last selected tab within the current app session.
1717
@Published public var selectedTab: ManagementTab = .settings
1818

19+
/// One-shot request to focus a specific sub-tab inside `VoiceView`.
20+
/// VoiceView observes this and resets it to nil after applying.
21+
@Published public var voiceSubTabRequest: String?
22+
1923
private init() {}
2024
}

0 commit comments

Comments
 (0)