Skip to content

Commit c604154

Browse files
committed
feat: SDK hardening, improved startup chain, callbacks, expose audio config & startup metrics
1 parent 7a6abc8 commit c604154

15 files changed

Lines changed: 1266 additions & 187 deletions

Sources/ElevenLabs/Auth/TokenService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,5 @@ enum TokenError: LocalizedError, Sendable {
204204
}
205205
}
206206
}
207+
208+
extension TokenService: TokenServicing {}

Sources/ElevenLabs/Conversation.swift

Lines changed: 492 additions & 131 deletions
Large diffs are not rendered by default.

Sources/ElevenLabs/DI/Dependencies.swift

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,45 @@
1+
import Foundation
12
import LiveKit
23

4+
protocol TokenServicing: Sendable {
5+
func fetchConnectionDetails(configuration: ElevenLabsConfiguration) async throws -> TokenService.ConnectionDetails
6+
}
7+
8+
@MainActor
9+
protocol ConnectionManaging: AnyObject {
10+
var onAgentReady: (() -> Void)? { get set }
11+
var onAgentDisconnected: (() -> Void)? { get set }
12+
var room: Room? { get }
13+
var shouldObserveRoomConnection: Bool { get }
14+
var errorHandler: (Error?) -> Void { get set }
15+
16+
func connect(
17+
details: TokenService.ConnectionDetails,
18+
enableMic: Bool,
19+
graceTimeout: TimeInterval,
20+
) async throws
21+
22+
func disconnect() async
23+
func dataEventsStream() -> AsyncStream<Data>
24+
func waitForAgentReady(timeout: TimeInterval) async -> AgentReadyWaitResult
25+
func publish(data: Data, options: DataPublishOptions) async throws
26+
}
27+
28+
@MainActor
29+
protocol ConversationDependencyProvider: AnyObject {
30+
var tokenService: any TokenServicing { get }
31+
var connectionManager: any ConnectionManaging { get }
32+
var errorHandler: (Error?) -> Void { get }
33+
}
34+
335
/// A minimalistic dependency injection container.
436
/// It allows sharing common dependencies e.g. `Room` between view models and services.
537
/// - Note: For production apps, consider using a more flexible approach offered by e.g.:
638
/// - [Factory](https://github.com/hmlongco/Factory)
739
/// - [swift-dependencies](https://github.com/pointfreeco/swift-dependencies)
840
/// - [Needle](https://github.com/uber/needle)
941
@MainActor
10-
final class Dependencies {
42+
final class Dependencies: ConversationDependencyProvider {
1143
static let shared = Dependencies()
1244

1345
private init() {}
@@ -18,7 +50,7 @@ final class Dependencies {
1850

1951
// MARK: Services
2052

21-
lazy var tokenService: TokenService = {
53+
lazy var tokenService: any TokenServicing = {
2254
let globalConfig = ElevenLabs.Global.shared.configuration
2355
let tokenServiceConfig = TokenService.Configuration(
2456
apiEndpoint: globalConfig.apiEndpoint?.absoluteString,
@@ -27,7 +59,7 @@ final class Dependencies {
2759
return TokenService(configuration: tokenServiceConfig)
2860
}()
2961

30-
lazy var connectionManager = ConnectionManager()
62+
lazy var connectionManager: any ConnectionManaging = ConnectionManager()
3163

3264
private lazy var localMessageSender = LocalMessageSender(room: room)
3365

Sources/ElevenLabs/ElevenLabs.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ public enum ElevenLabs {
194194
// Protocol event types are already public from their respective files
195195
// Re-export AgentState from LiveKit for SDK users
196196
public typealias AgentState = LiveKit.AgentState
197+
public typealias SpeechActivityEvent = LiveKit.SpeechActivityEvent
198+
public typealias MicrophoneMuteMode = LiveKit.MicrophoneMuteMode
197199

198200
// Re-export audio track types for advanced audio handling
199201
public typealias LocalAudioTrack = LiveKit.LocalAudioTrack
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
import LiveKit
3+
4+
/// Configures microphone pipeline and voice activity reporting exposed by the SDK.
5+
public struct AudioPipelineConfiguration: Sendable {
6+
/// Override the microphone mute strategy. Defaults to `.inputMixer` to match previous SDK behaviour.
7+
public var microphoneMuteMode: MicrophoneMuteMode?
8+
9+
/// Keep the recording engine warm to avoid first-spoken-word latency. Defaults to `true`.
10+
public var recordingAlwaysPrepared: Bool?
11+
12+
/// Bypass WebRTC voice processing (AEC/NS/VAD). Leave `nil` to preserve system defaults.
13+
public var voiceProcessingBypassed: Bool?
14+
15+
/// Toggle Auto Gain Control. Leave `nil` to preserve system defaults.
16+
public var voiceProcessingAGCEnabled: Bool?
17+
18+
/// Observe LiveKit speech activity events while the microphone is muted.
19+
public var onSpeechActivity: (@Sendable (SpeechActivityEvent) -> Void)?
20+
21+
public init(
22+
microphoneMuteMode: MicrophoneMuteMode? = .inputMixer,
23+
recordingAlwaysPrepared: Bool? = true,
24+
voiceProcessingBypassed: Bool? = nil,
25+
voiceProcessingAGCEnabled: Bool? = nil,
26+
onSpeechActivity: (@Sendable (SpeechActivityEvent) -> Void)? = nil,
27+
) {
28+
self.microphoneMuteMode = microphoneMuteMode
29+
self.recordingAlwaysPrepared = recordingAlwaysPrepared
30+
self.voiceProcessingBypassed = voiceProcessingBypassed
31+
self.voiceProcessingAGCEnabled = voiceProcessingAGCEnabled
32+
self.onSpeechActivity = onSpeechActivity
33+
}
34+
35+
public static let `default` = AudioPipelineConfiguration()
36+
}
37+
38+
extension MicrophoneMuteMode: @retroactive @unchecked Sendable {}
39+
extension SpeechActivityEvent: @retroactive @unchecked Sendable {}

Sources/ElevenLabs/Models/ConversationConfig.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import LiveKit
23

34
/// Main configuration for a conversation session
45
public struct ConversationConfig: Sendable {
@@ -15,6 +16,51 @@ public struct ConversationConfig: Sendable {
1516
/// Called when the agent disconnects or the conversation ends
1617
public var onDisconnect: (@Sendable () -> Void)?
1718

19+
/// Called whenever the startup state transitions
20+
public var onStartupStateChange: (@Sendable (ConversationStartupState) -> Void)?
21+
22+
/// Controls timings and retry behavior for the initialization handshake
23+
public var startupConfiguration: ConversationStartupConfiguration
24+
25+
/// Controls microphone pipeline behaviour and VAD callbacks.
26+
public var audioConfiguration: AudioPipelineConfiguration?
27+
28+
/// Called when a startup-related error occurs
29+
public var onError: (@Sendable (ConversationError) -> Void)?
30+
31+
/// Called when LiveKit detects speech activity while muted.
32+
public var onSpeechActivity: (@Sendable (SpeechActivityEvent) -> Void)?
33+
34+
/// Called for each agent response with the associated event identifier.
35+
public var onAgentResponse: (@Sendable (_ text: String, _ eventId: Int) -> Void)?
36+
37+
/// Called when an agent response correction is received.
38+
public var onAgentResponseCorrection: (@Sendable (_ original: String, _ corrected: String, _ eventId: Int) -> Void)?
39+
40+
/// Called for each user transcript event.
41+
public var onUserTranscript: (@Sendable (_ text: String, _ eventId: Int) -> Void)?
42+
43+
/// Called when conversation metadata arrives.
44+
public var onConversationMetadata: (@Sendable (ConversationMetadataEvent) -> Void)?
45+
46+
/// Called when the agent emits a tool response event.
47+
public var onAgentToolResponse: (@Sendable (AgentToolResponseEvent) -> Void)?
48+
49+
/// Called when the agent detects an interruption.
50+
public var onInterruption: (@Sendable (_ eventId: Int) -> Void)?
51+
52+
/// Called whenever a VAD score is emitted.
53+
public var onVadScore: (@Sendable (_ score: Double) -> Void)?
54+
55+
/// Called when audio alignment metadata is emitted.
56+
public var onAudioAlignment: (@Sendable (AudioAlignment) -> Void)?
57+
58+
/// Called when feedback availability changes.
59+
public var onCanSendFeedbackChange: (@Sendable (Bool) -> Void)?
60+
61+
/// Called when a client tool call is received without a registered handler.
62+
public var onUnhandledClientToolCall: (@Sendable (ClientToolCallEvent) -> Void)?
63+
1864
public init(
1965
agentOverrides: AgentOverrides? = nil,
2066
ttsOverrides: TTSOverrides? = nil,
@@ -24,6 +70,21 @@ public struct ConversationConfig: Sendable {
2470
userId: String? = nil,
2571
onAgentReady: (@Sendable () -> Void)? = nil,
2672
onDisconnect: (@Sendable () -> Void)? = nil,
73+
onStartupStateChange: (@Sendable (ConversationStartupState) -> Void)? = nil,
74+
startupConfiguration: ConversationStartupConfiguration = .default,
75+
audioConfiguration: AudioPipelineConfiguration? = nil,
76+
onError: (@Sendable (ConversationError) -> Void)? = nil,
77+
onSpeechActivity: (@Sendable (SpeechActivityEvent) -> Void)? = nil,
78+
onAgentResponse: (@Sendable (_ text: String, _ eventId: Int) -> Void)? = nil,
79+
onAgentResponseCorrection: (@Sendable (_ original: String, _ corrected: String, _ eventId: Int) -> Void)? = nil,
80+
onUserTranscript: (@Sendable (_ text: String, _ eventId: Int) -> Void)? = nil,
81+
onConversationMetadata: (@Sendable (ConversationMetadataEvent) -> Void)? = nil,
82+
onAgentToolResponse: (@Sendable (AgentToolResponseEvent) -> Void)? = nil,
83+
onInterruption: (@Sendable (_ eventId: Int) -> Void)? = nil,
84+
onVadScore: (@Sendable (_ score: Double) -> Void)? = nil,
85+
onAudioAlignment: (@Sendable (AudioAlignment) -> Void)? = nil,
86+
onCanSendFeedbackChange: (@Sendable (Bool) -> Void)? = nil,
87+
onUnhandledClientToolCall: (@Sendable (ClientToolCallEvent) -> Void)? = nil,
2788
) {
2889
self.agentOverrides = agentOverrides
2990
self.ttsOverrides = ttsOverrides
@@ -33,6 +94,21 @@ public struct ConversationConfig: Sendable {
3394
self.userId = userId
3495
self.onAgentReady = onAgentReady
3596
self.onDisconnect = onDisconnect
97+
self.onStartupStateChange = onStartupStateChange
98+
self.startupConfiguration = startupConfiguration
99+
self.audioConfiguration = audioConfiguration
100+
self.onError = onError
101+
self.onSpeechActivity = onSpeechActivity
102+
self.onAgentResponse = onAgentResponse
103+
self.onAgentResponseCorrection = onAgentResponseCorrection
104+
self.onUserTranscript = onUserTranscript
105+
self.onConversationMetadata = onConversationMetadata
106+
self.onAgentToolResponse = onAgentToolResponse
107+
self.onInterruption = onInterruption
108+
self.onVadScore = onVadScore
109+
self.onAudioAlignment = onAudioAlignment
110+
self.onCanSendFeedbackChange = onCanSendFeedbackChange
111+
self.onUnhandledClientToolCall = onUnhandledClientToolCall
36112
}
37113
}
38114

@@ -101,6 +177,21 @@ extension ConversationConfig {
101177
userId: userId,
102178
onAgentReady: onAgentReady,
103179
onDisconnect: onDisconnect,
180+
onStartupStateChange: onStartupStateChange,
181+
startupConfiguration: startupConfiguration,
182+
audioConfiguration: audioConfiguration,
183+
onError: onError,
184+
onSpeechActivity: onSpeechActivity,
185+
onAgentResponse: onAgentResponse,
186+
onAgentResponseCorrection: onAgentResponseCorrection,
187+
onUserTranscript: onUserTranscript,
188+
onConversationMetadata: onConversationMetadata,
189+
onAgentToolResponse: onAgentToolResponse,
190+
onInterruption: onInterruption,
191+
onVadScore: onVadScore,
192+
onAudioAlignment: onAudioAlignment,
193+
onCanSendFeedbackChange: onCanSendFeedbackChange,
194+
onUnhandledClientToolCall: onUnhandledClientToolCall,
104195
)
105196
}
106197
}

0 commit comments

Comments
 (0)