feat: voice and model pickers with sync-on-join (Phase 4)
- Voices.swift: 13 Azure voices, default en-US-Ava - SettingsStore: persist voiceID/modelID, fallback to default voice - VoiceSession: sync set_voice/set_model on agent_ready, selectVoice/selectModel, track models + model_changed - SettingsView: voice and model picker sheets - ControlChannel: publish via publish(data:) reliable data channel instead of sendText DataStream (agent listens on legacy data_received; DataStream text was dropped)
This commit is contained in:
1 parent
5483e6ed48
commit
ca2d26f171
8 files changed
+256
-14
No files matched your search
@@ -26,6 +26,8 @@ final class VoiceSession: ObservableObject {
|
||||
@Published private(set) var agent: AgentInfo?
|
||||
@Published private(set) var isMuted = false
|
||||
@Published private(set) var micError: String?
|
||||
@Published private(set) var models: [ControlChannel.ModelInfo] = []
|
||||
@Published private(set) var selectedModel: String?
|
||||
|
||||
private(set) var room = Room()
|
||||
let controlChannel = ControlChannel()
|
||||
@@ -33,18 +35,40 @@ final class VoiceSession: ObservableObject {
|
||||
let stateModel = ConversationStateModel()
|
||||
private let config: ServerConfig
|
||||
private let tokenClient: TokenClient
|
||||
private let settings: SettingsStore
|
||||
private var reconnectAttempted = false
|
||||
private var connectAttempts = 0
|
||||
private var agentWaitTask: Task<Void, Never>?
|
||||
|
||||
init(config: ServerConfig, tokenClient: TokenClient) {
|
||||
init(config: ServerConfig, tokenClient: TokenClient, settings: SettingsStore) {
|
||||
self.config = config
|
||||
self.tokenClient = tokenClient
|
||||
self.settings = settings
|
||||
room.add(delegate: self)
|
||||
room.add(delegate: controlChannel)
|
||||
room.add(delegate: transcriptHandler)
|
||||
controlChannel.onEvent = { [weak self] _ in
|
||||
guard self != nil else { return }
|
||||
controlChannel.onEvent = { [weak self] event in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.handleControlEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleControlEvent(_ event: ControlChannel.InboundEvent) {
|
||||
switch event {
|
||||
case .models(let received, let selected):
|
||||
models = received
|
||||
if selectedModel == nil, let selected {
|
||||
selectedModel = selected
|
||||
settings.modelID = selected
|
||||
}
|
||||
logger.log("models received count=\(received.count) selected=\(selected ?? "nil", privacy: .public)")
|
||||
case .modelChanged(let model):
|
||||
selectedModel = model
|
||||
settings.modelID = model
|
||||
logger.log("model_changed applied: \(model, privacy: .public)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +168,44 @@ final class VoiceSession: ObservableObject {
|
||||
stateModel.start()
|
||||
logger.log("state=ready, enabling mic")
|
||||
Task { await enableMicrophone() }
|
||||
Task { await syncSettingsOnJoin() }
|
||||
}
|
||||
|
||||
private func syncSettingsOnJoin() async {
|
||||
guard isReady else { return }
|
||||
let voiceID = settings.voiceID
|
||||
do {
|
||||
try await controlChannel.setVoice(voiceID, room: room)
|
||||
logger.log("sync-on-join set_voice sent: \(voiceID, privacy: .public)")
|
||||
if let model = selectedModel ?? settings.modelID {
|
||||
try await controlChannel.setModel(model, room: room)
|
||||
logger.log("sync-on-join set_model sent: \(model, privacy: .public)")
|
||||
}
|
||||
} catch {
|
||||
logger.error("sync-on-join failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func selectVoice(_ id: String) async {
|
||||
settings.voiceID = id
|
||||
guard isReady else { return }
|
||||
do {
|
||||
try await controlChannel.setVoice(id, room: room)
|
||||
logger.log("set_voice sent: \(id, privacy: .public)")
|
||||
} catch {
|
||||
logger.error("set_voice failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func selectModel(_ id: String) async {
|
||||
settings.modelID = id
|
||||
guard isReady else { return }
|
||||
do {
|
||||
try await controlChannel.setModel(id, room: room)
|
||||
logger.log("set_model sent: \(id, privacy: .public)")
|
||||
} catch {
|
||||
logger.error("set_model failed: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private func scanForAgent() {
|
||||
@@ -216,6 +278,7 @@ final class VoiceSession: ObservableObject {
|
||||
agent = nil
|
||||
isMuted = false
|
||||
micError = nil
|
||||
models = []
|
||||
reconnectAttempted = false
|
||||
stateModel.stop()
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
|
||||
Reference in new issue
Block a user