UNPKG

@inworld/web-core

Version:
238 lines (237 loc) 11 kB
import { v4 } from 'uuid'; import { DataChunkDataType, } from '../../proto/ai/inworld/packets/packets.pb.js'; import { AudioSessionState, ConversationParticipant, ConversationState, TtsPlaybackAction, } from '../common/data_structures/index.js'; import { MULTI_CHAR_NARRATED_ACTIONS } from '../common/errors.js'; export class ConversationService { constructor(connection, { participants, conversationId, addCharacters, startRecording, stopRecording, }) { this.packetQueue = []; this.ttsPlaybackAction = TtsPlaybackAction.UNKNOWN; this.connection = connection; this.conversationId = conversationId !== null && conversationId !== void 0 ? conversationId : v4(); this.participants = participants; this.addCharacters = addCharacters; this.startRecording = startRecording; this.stopRecording = stopRecording; } getConversationId() { return this.conversationId; } getParticipants() { return this.participants; } getCharacters() { return this.connection.getCharactersByResourceNames(this.getCharacterParticipants()); } getHistory() { return this.connection.history.get(this.getConversationId()); } getTranscript() { return this.connection.history.getTranscript(this.getConversationId()); } changeParticipants(participants) { this.participants = participants; } async updateParticipants(participants) { var _a, _b; const conversationId = this.getConversationId(); let conversation = this.connection.conversations.get(conversationId); if (!conversation) { throw Error(`Conversation ${conversationId} not found`); } if (![ConversationState.ACTIVE, ConversationState.INACTIVE].includes(conversation.state)) { return; } this.connection.conversations.set(conversationId, { service: conversation.service, state: ConversationState.PROCESSING, }); conversation = this.connection.conversations.get(conversationId); let needToReacreateAudioSession = false; // If audio session is started, we need to end it before updating participants if (this.connection.getAudioSessionAction() === AudioSessionState.START) { needToReacreateAudioSession = true; (_a = this.stopRecording) === null || _a === void 0 ? void 0 : _a.call(this); this.beforeAudioSessionEnd(); await this.connection.send(() => this.connection.getEventFactory().audioSessionEnd({ conversationId })); } // Load characters if they are not loaded const charactersNamesOnly = this.getCharacterParticipants(participants); let characters = await this.connection.getCharacters(); const charactersToAdd = charactersNamesOnly.filter((p) => !characters.find((c) => c.resourceName === p)); if (charactersToAdd.length) { await this.addCharacters(charactersToAdd); characters = await this.connection.getCharacters(); } characters = characters.filter((c) => charactersNamesOnly.includes(c.resourceName)); // Update conversation const conversationParticipants = characters.map((c) => c.id); if (participants.includes(ConversationParticipant.USER)) { conversationParticipants.push(ConversationParticipant.USER); } const sent = await this.connection.send(() => this.connection.getEventFactory().conversation(conversationParticipants, { conversationId: this.getConversationId(), })); // If audio session was started before, we need to restart it if (needToReacreateAudioSession) { this.beforeAudioSessionStart(); await this.connection.send(() => this.connection.getEventFactory().audioSessionStart({ conversationId })); (_b = this.startRecording) === null || _b === void 0 ? void 0 : _b.call(this); } await this.resolveInterval(() => { const found = this.connection.conversations.get(sent.packetId.conversationId); return (found === null || found === void 0 ? void 0 : found.state) === ConversationState.ACTIVE; }, () => { this.participants = participants; this.releaseQueue(); }); return conversation.service; } async sendText(text) { return this.ensureConversation(() => this.connection .getEventFactory() .text(text, { conversationId: this.getConversationId() })); } async sendAudio(chunk) { return this.ensureConversation(() => this.connection .getEventFactory() .dataChunk(chunk, DataChunkDataType.AUDIO, { conversationId: this.getConversationId(), })); } async sendTrigger(name, parameters) { return this.ensureConversation(() => this.connection.getEventFactory().trigger(name, Object.assign(Object.assign({}, parameters), { conversationId: this.getConversationId() }))); } async sendAudioSessionStart(params, force) { if (!force && this.connection.getAudioSessionAction() === AudioSessionState.START) { throw Error('Audio session is already started'); } this.beforeAudioSessionStart(); return this.ensureConversation(() => this.connection.getEventFactory().audioSessionStart(Object.assign(Object.assign({}, params), { conversationId: this.getConversationId() }))); } async sendAudioSessionEnd(force) { if (!force && this.connection.getAudioSessionAction() !== AudioSessionState.START) { throw Error('Audio session cannot be ended because it has not been started'); } return this.ensureConversation(() => { this.beforeAudioSessionEnd(); return this.connection .getEventFactory() .audioSessionEnd({ conversationId: this.getConversationId() }); }); } async sendCancelResponse(cancelResponses) { return this.ensureConversation(() => this.connection.getEventFactory().cancelResponse(cancelResponses)); } async sendTTSPlaybackMute(isMuted) { return this.ensureConversation(() => { this.setTtsPlaybackAction(isMuted ? TtsPlaybackAction.MUTE : TtsPlaybackAction.UNMUTE); return this.connection.getEventFactory().mutePlayback(isMuted, { conversationId: this.getConversationId(), }); }, { skipMuting: false }); } async sendNarratedAction(text) { if (this.getCharacterParticipants().length > 1) { throw Error(MULTI_CHAR_NARRATED_ACTIONS); } return this.ensureConversation(() => this.connection.getEventFactory().narratedAction(text, { conversationId: this.getConversationId(), })); } async sendCustomPacket(getPacket) { return this.ensureConversation(() => getPacket({ conversationId: this.getConversationId(), })); } async sendPerceivedLatenctReport(props) { return this.ensureConversation(() => this.connection.getEventFactory().perceivedLatency(props)); } async ensureConversation(getPacket, props) { const conversationId = this.getConversationId(); const conversation = this.connection.conversations.get(conversationId); if (!conversation) { throw Error(`Conversation ${conversationId} not found`); } if (conversation.state === ConversationState.ACTIVE) { return this.connection.send(getPacket); } else if (conversation.state === ConversationState.PROCESSING) { let packet; this.packetQueue.push({ getPacket, afterWriting: (inworldPacket) => { packet = inworldPacket; }, }); return this.resolveInterval(() => !!packet, () => packet); } this.connection.conversations.set(this.getConversationId(), { service: conversation.service, state: ConversationState.PROCESSING, }); const conversationParticipants = conversation.service .getCharacters() .map((c) => c.id); if (conversation.service .getParticipants() .includes(ConversationParticipant.USER)) { conversationParticipants.push(ConversationParticipant.USER); } const conversationPacket = await this.connection.send(() => this.connection.getEventFactory().conversation(conversationParticipants, { conversationId: this.getConversationId(), })); await this.resolveInterval(() => { const found = this.connection.conversations.get(conversationPacket.packetId.conversationId); return (found === null || found === void 0 ? void 0 : found.state) === ConversationState.ACTIVE; }); if (this.connection.isAutoReconnected() && this.getTtsPlaybackAction() === TtsPlaybackAction.MUTE && !(props === null || props === void 0 ? void 0 : props.skipMuting)) { await this.connection.send(() => this.connection.getEventFactory().mutePlayback(true, { conversationId: this.getConversationId(), })); } const sent = await this.connection.send(getPacket); this.releaseQueue(); return sent; } beforeAudioSessionStart() { this.connection.setAudioSessionAction(AudioSessionState.START); this.connection.setCurrentAudioConversation(this); } beforeAudioSessionEnd() { this.connection.setAudioSessionAction(AudioSessionState.END); this.connection.setCurrentAudioConversation(undefined); } setTtsPlaybackAction(action) { this.ttsPlaybackAction = action; this.connection.history.setAudioEnabled(this.getConversationId(), action === TtsPlaybackAction.UNMUTE); } getTtsPlaybackAction() { return this.ttsPlaybackAction; } async resolveInterval(done, resolve) { return new Promise((r) => { const interval = setInterval(() => { if (done()) { clearInterval(interval); this.connection.removeInterval(interval); r(resolve === null || resolve === void 0 ? void 0 : resolve()); } }, 10); this.connection.addInterval(interval); }); } releaseQueue() { this.packetQueue.forEach(async (item) => { const inworldPacket = await this.connection.send(item.getPacket); item.afterWriting(inworldPacket); }); this.packetQueue = []; } getCharacterParticipants(participants = this.participants) { return participants.filter((p) => p !== ConversationParticipant.USER); } }