feat: add expo-two-way-audio module to workspace
This commit is contained in:
@@ -2130,7 +2130,12 @@ export class Session {
|
||||
agentId?: string,
|
||||
requestId?: string
|
||||
): Promise<void> {
|
||||
const startedAt = Date.now()
|
||||
try {
|
||||
this.sessionLogger.info(
|
||||
{ enabled, requestedAgentId: agentId ?? null, requestId: requestId ?? null },
|
||||
'set_voice_mode started'
|
||||
)
|
||||
if (enabled) {
|
||||
const unavailable = this.resolveVoiceFeatureUnavailableContext('voice_mode')
|
||||
if (unavailable) {
|
||||
@@ -2144,19 +2149,44 @@ export class Session {
|
||||
this.voiceModeAgentId &&
|
||||
this.voiceModeAgentId !== normalizedAgentId
|
||||
) {
|
||||
this.sessionLogger.info(
|
||||
{
|
||||
previousAgentId: this.voiceModeAgentId,
|
||||
nextAgentId: normalizedAgentId,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
},
|
||||
'set_voice_mode disabling previous active voice agent'
|
||||
)
|
||||
await this.disableVoiceModeForActiveAgent(true)
|
||||
}
|
||||
|
||||
if (!this.isVoiceMode || this.voiceModeAgentId !== normalizedAgentId) {
|
||||
this.sessionLogger.info(
|
||||
{ agentId: normalizedAgentId, elapsedMs: Date.now() - startedAt },
|
||||
'set_voice_mode enabling voice for agent'
|
||||
)
|
||||
const refreshedAgentId = await this.enableVoiceModeForAgent(normalizedAgentId)
|
||||
this.voiceModeAgentId = refreshedAgentId
|
||||
this.sessionLogger.info(
|
||||
{ agentId: refreshedAgentId, elapsedMs: Date.now() - startedAt },
|
||||
'set_voice_mode agent enable complete'
|
||||
)
|
||||
}
|
||||
|
||||
this.sessionLogger.info(
|
||||
{ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt },
|
||||
'set_voice_mode starting voice turn controller'
|
||||
)
|
||||
await this.startVoiceTurnController()
|
||||
this.sessionLogger.info(
|
||||
{ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt },
|
||||
'set_voice_mode voice turn controller started'
|
||||
)
|
||||
this.isVoiceMode = true
|
||||
this.sessionLogger.info(
|
||||
{
|
||||
agentId: this.voiceModeAgentId,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
},
|
||||
'Voice mode enabled for existing agent'
|
||||
)
|
||||
@@ -2175,9 +2205,13 @@ export class Session {
|
||||
return
|
||||
}
|
||||
|
||||
this.sessionLogger.info(
|
||||
{ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt },
|
||||
'set_voice_mode disabling active voice mode'
|
||||
)
|
||||
await this.disableVoiceModeForActiveAgent(true)
|
||||
this.isVoiceMode = false
|
||||
this.sessionLogger.info('Voice mode disabled')
|
||||
this.sessionLogger.info({ elapsedMs: Date.now() - startedAt }, 'Voice mode disabled')
|
||||
if (requestId) {
|
||||
this.emit({
|
||||
type: 'set_voice_mode_response',
|
||||
@@ -2198,6 +2232,7 @@ export class Session {
|
||||
err: error,
|
||||
enabled,
|
||||
requestedAgentId: agentId ?? null,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
},
|
||||
'set_voice_mode failed'
|
||||
)
|
||||
@@ -2256,14 +2291,25 @@ export class Session {
|
||||
}
|
||||
|
||||
private async enableVoiceModeForAgent(agentId: string): Promise<string> {
|
||||
const startedAt = Date.now()
|
||||
const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent
|
||||
if (!ensureVoiceSocket) {
|
||||
throw new Error('Voice MCP socket bridge is not configured')
|
||||
}
|
||||
|
||||
this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureAgentLoaded.start')
|
||||
const existing = await this.ensureAgentLoaded(agentId)
|
||||
this.sessionLogger.info(
|
||||
{ agentId, elapsedMs: Date.now() - startedAt },
|
||||
'enableVoiceModeForAgent.ensureAgentLoaded.done'
|
||||
)
|
||||
|
||||
this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureVoiceSocket.start')
|
||||
const socketPath = await ensureVoiceSocket(agentId)
|
||||
this.sessionLogger.info(
|
||||
{ agentId, socketPath, elapsedMs: Date.now() - startedAt },
|
||||
'enableVoiceModeForAgent.ensureVoiceSocket.done'
|
||||
)
|
||||
this.registerVoiceBridgeForAgent(agentId)
|
||||
|
||||
const baseConfig: VoiceModeBaseConfig = {
|
||||
@@ -2277,7 +2323,15 @@ export class Session {
|
||||
}
|
||||
|
||||
try {
|
||||
this.sessionLogger.info(
|
||||
{ agentId, elapsedMs: Date.now() - startedAt },
|
||||
'enableVoiceModeForAgent.reloadAgentSession.start'
|
||||
)
|
||||
const refreshed = await this.agentManager.reloadAgentSession(agentId, refreshOverrides)
|
||||
this.sessionLogger.info(
|
||||
{ agentId, refreshedAgentId: refreshed.id, elapsedMs: Date.now() - startedAt },
|
||||
'enableVoiceModeForAgent.reloadAgentSession.done'
|
||||
)
|
||||
return refreshed.id
|
||||
} catch (error) {
|
||||
this.unregisterVoiceSpeakHandler?.(agentId)
|
||||
@@ -2331,6 +2385,7 @@ export class Session {
|
||||
|
||||
private async startVoiceTurnController(): Promise<void> {
|
||||
if (this.voiceTurnController) {
|
||||
this.sessionLogger.info('startVoiceTurnController skipped: already running')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2339,6 +2394,11 @@ export class Session {
|
||||
throw new Error('Voice turn detection is not configured')
|
||||
}
|
||||
|
||||
this.sessionLogger.info(
|
||||
{ providerId: turnDetection.id },
|
||||
'startVoiceTurnController creating controller'
|
||||
)
|
||||
|
||||
const controller = createVoiceTurnController({
|
||||
logger: this.sessionLogger.child({ component: 'voice-turn-controller' }),
|
||||
turnDetection,
|
||||
@@ -2381,8 +2441,10 @@ export class Session {
|
||||
},
|
||||
})
|
||||
|
||||
this.sessionLogger.info('startVoiceTurnController connecting controller')
|
||||
await controller.start()
|
||||
this.voiceTurnController = controller
|
||||
this.sessionLogger.info('startVoiceTurnController connected')
|
||||
}
|
||||
|
||||
private async stopVoiceTurnController(): Promise<void> {
|
||||
@@ -6368,12 +6430,9 @@ export class Session {
|
||||
}
|
||||
|
||||
private async processCompletedAudio(audio: Buffer, format: string): Promise<void> {
|
||||
const shouldBuffer =
|
||||
this.processingPhase === 'transcribing' && this.pendingAudioSegments.length === 0
|
||||
|
||||
if (shouldBuffer) {
|
||||
if (this.processingPhase === 'transcribing') {
|
||||
this.sessionLogger.debug(
|
||||
{ phase: this.processingPhase },
|
||||
{ phase: this.processingPhase, segmentCount: this.pendingAudioSegments.length + 1 },
|
||||
`Buffering audio segment (phase: ${this.processingPhase})`
|
||||
)
|
||||
this.pendingAudioSegments.push({
|
||||
@@ -6408,6 +6467,26 @@ export class Session {
|
||||
await this.processAudio(audio, format)
|
||||
}
|
||||
|
||||
private async flushPendingAudioSegments(reason: string): Promise<void> {
|
||||
if (this.processingPhase === 'transcribing' || this.pendingAudioSegments.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pendingSegments = [...this.pendingAudioSegments]
|
||||
this.pendingAudioSegments = []
|
||||
this.clearBufferTimeout()
|
||||
|
||||
this.sessionLogger.debug(
|
||||
{ reason, segmentCount: pendingSegments.length },
|
||||
`Flushing ${pendingSegments.length} buffered audio segment(s)`
|
||||
)
|
||||
|
||||
const combinedAudio = Buffer.concat(pendingSegments.map((segment) => segment.audio))
|
||||
const combinedFormat = pendingSegments[pendingSegments.length - 1]!.format
|
||||
|
||||
await this.processAudio(combinedAudio, combinedFormat)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process audio through STT and then LLM
|
||||
*/
|
||||
@@ -6456,6 +6535,7 @@ export class Session {
|
||||
} catch (error: any) {
|
||||
this.setPhase('idle')
|
||||
this.clearSpeechInProgress('transcription error')
|
||||
await this.flushPendingAudioSegments('transcription error')
|
||||
this.emit({
|
||||
type: 'activity_log',
|
||||
payload: {
|
||||
@@ -6495,6 +6575,7 @@ export class Session {
|
||||
this.sessionLogger.debug('Empty transcription (false positive), not aborting')
|
||||
this.setPhase('idle')
|
||||
this.clearSpeechInProgress('empty transcription')
|
||||
await this.flushPendingAudioSegments('empty transcription')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6539,6 +6620,7 @@ export class Session {
|
||||
{ requestId: result.requestId },
|
||||
'Skipping voice agent processing because voice mode is disabled'
|
||||
)
|
||||
await this.flushPendingAudioSegments('voice mode disabled')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6548,12 +6630,14 @@ export class Session {
|
||||
{ requestId: result.requestId },
|
||||
'Skipping voice agent processing because no agent is currently voice-enabled'
|
||||
)
|
||||
await this.flushPendingAudioSegments('no active voice agent')
|
||||
return
|
||||
}
|
||||
|
||||
// Route voice utterances through the same send path as regular text input:
|
||||
// interrupt-if-running, record message, then start a new stream.
|
||||
await this.handleSendAgentMessage(agentId, result.text)
|
||||
await this.flushPendingAudioSegments('transcription complete')
|
||||
}
|
||||
|
||||
private registerVoiceBridgeForAgent(agentId: string): void {
|
||||
@@ -6723,6 +6807,15 @@ export class Session {
|
||||
this.bufferTimeout = setTimeout(async () => {
|
||||
this.sessionLogger.debug('Buffer timeout reached, processing pending segments')
|
||||
|
||||
if (this.processingPhase === 'transcribing') {
|
||||
this.sessionLogger.debug(
|
||||
{ segmentCount: this.pendingAudioSegments.length },
|
||||
'Buffer timeout deferred because transcription is still in progress'
|
||||
)
|
||||
this.setBufferTimeout()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.pendingAudioSegments.length > 0) {
|
||||
const segments = [...this.pendingAudioSegments]
|
||||
this.pendingAudioSegments = []
|
||||
|
||||
@@ -278,4 +278,5 @@ describe("voice turn controller", () => {
|
||||
deferred.resolve();
|
||||
await settleSerialQueue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -117,6 +117,14 @@ export function createVoiceTurnController(params: {
|
||||
rollingPrefixBytes: prefixBuffer.byteLength,
|
||||
utteranceBytes: prefix.length,
|
||||
};
|
||||
params.logger.info(
|
||||
{
|
||||
utteranceId: state.utteranceId,
|
||||
prefixBytes: prefix.length,
|
||||
rollingPrefixBytes: prefixBuffer.byteLength,
|
||||
},
|
||||
"voice_turn.speech_started"
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSpeechStopped(): Promise<void> {
|
||||
@@ -133,6 +141,14 @@ export function createVoiceTurnController(params: {
|
||||
|
||||
await params.callbacks.onSpeechStopped();
|
||||
|
||||
params.logger.info(
|
||||
{
|
||||
utteranceBytes: utterance.length,
|
||||
utteranceAgeMs: Math.max(0, endedAt - startedAt),
|
||||
},
|
||||
"voice_turn.speech_stopped"
|
||||
);
|
||||
|
||||
if (utterance.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user