UNPKG

@inworld/web-core

Version:
325 lines (324 loc) 14.1 kB
import { AudioSessionState, ConversationIntializeState, ConversationParticipant, ConversationState, } from '../common/data_structures/index.js'; import { CHARACTER_HAS_INVALID_FORMAT, CURRENT_CHARACTER_NOT_SET, SCENE_HAS_INVALID_FORMAT, } from '../common/errors.js'; import { InworldPlayer } from '../components/sound/inworld_player.js'; import { InworldRecorder } from '../components/sound/inworld_recorder.js'; import { InworldError } from '../entities/error.entity.js'; import { characterHasValidFormat, sceneHasValidFormat } from '../guard/scene.js'; import { ConversationService } from './conversation.service.js'; import { EntityService } from './entity.service.js'; import { SessionStateService } from './session_state.service.js'; import { FeedbackService } from './wrappers/feedback.service.js'; import { StateSerializationService } from './wrappers/state_serialization.service.js'; export class InworldConnectionService { constructor(props) { this.oneToOneConversationIntializeState = ConversationIntializeState.INACTIVE; this.connection = props.connection; this.grpcAudioPlayer = props.grpcAudioPlayer; this.feedback = new FeedbackService(props.connection); this.entity = new EntityService(props.connection); this.stateSerialization = new StateSerializationService(props.connection); this.player = new InworldPlayer({ grpcAudioPlayer: this.grpcAudioPlayer, }); this.recorder = new InworldRecorder({ listener: this.recorderListener.bind(this), grpcAudioPlayer: this.grpcAudioPlayer, grpcAudioRecorder: props.grpcAudioRecorder, webRtcLoopbackBiDiSession: props.webRtcLoopbackBiDiSession, }); this.sessionState = new SessionStateService(this.connection, this.stateSerialization); } async getSessionState() { console.warn('getSessionState is deprecated. Please use stateSerialization.get instead.'); return this.stateSerialization.get(); } async open() { return this.connection.openManually(); } async close() { // Stop recorder and close connection. this.recorder.stop(); this.connection.close(); // Stop player and clear all audio chunks. await this.player.stop(); this.player.clear(); this.sessionState.destroy(); } isActive() { return this.connection.isActive(); } getCapabilities() { return this.connection.getClientConfig().capabilities; } async getCharacters() { return this.connection.getCharacters(); } async getCurrentCharacter() { return this.connection.getCurrentCharacter(); } getCharacterById(id) { return this.connection.getCharactersByIds([id])[0]; } getCharacterByResourceName(name) { return this.connection.getCharactersByResourceNames([name])[0]; } async setCurrentCharacter(character) { this.connection.setCurrentCharacter(character); if (!this.oneToOneConversation) { this.oneToOneConversation = new ConversationService(this.connection, { participants: [character.resourceName, ConversationParticipant.USER], addCharacters: this.addCharacters.bind(this), startRecording: this.recorder.start.bind(this.recorder), stopRecording: this.recorder.stop.bind(this.recorder), }); this.addConversationToConnection(this.oneToOneConversation); } else { this.oneToOneConversation.changeParticipants([character.resourceName]); } if (this.connection.conversations.get(this.oneToOneConversation.getConversationId()).state === ConversationState.ACTIVE) { await this.oneToOneConversation.updateParticipants([ character.resourceName, ]); } } clearState() { this.sessionState.clear(); } getHistory() { var _a, _b; return (_b = (_a = this.oneToOneConversation) === null || _a === void 0 ? void 0 : _a.getHistory()) !== null && _b !== void 0 ? _b : []; } getFullHistory() { return this.connection.getHistory(); } clearHistory() { var _a, _b; const diff = this.getHistory(); this.connection.clearHistory(); if (diff.length > 0) { (_b = (_a = this.connection).onHistoryChange) === null || _b === void 0 ? void 0 : _b.call(_a, [], { diff: { removed: diff } }); } } getTranscript() { var _a, _b; return (_b = (_a = this.oneToOneConversation) === null || _a === void 0 ? void 0 : _a.getTranscript()) !== null && _b !== void 0 ? _b : ''; } getFullTranscript() { return this.connection.getTranscript(); } async getCurrentConversation() { await this.ensureOneToOneConversation(); return this.oneToOneConversation; } getConversations() { return [...this.connection.conversations.entries()].map(([conversationId, conversation]) => ({ conversationId, characters: conversation.service.getCharacters(), })); } async sendText(text) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendText(text); } async sendAudio(chunk) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendAudio(chunk); } async sendTrigger(name, parameters) { await this.ensureOneToOneConversation(); const character = await this.getCurrentCharacter(); if (parameters && Array.isArray(parameters)) { // TODO: Remove this deprecation warning in the next major release. console.warn('Passing parameters as an array is deprecated. Please use an object instead.'); return this.oneToOneConversation.sendTrigger(name, { parameters, character, }); } else if (!parameters || !Array.isArray(parameters)) { return this.oneToOneConversation.sendTrigger(name, { parameters: parameters && !Array.isArray(parameters) ? parameters.parameters : undefined, character, }); } } async sendAudioSessionStart(params) { if (this.connection.getAudioSessionAction() === AudioSessionState.START) { throw Error('Audio session is already started'); } this.connection.setAudioSessionAction(AudioSessionState.START); await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendAudioSessionStart(params, true); } async sendAudioSessionEnd() { if (this.connection.getAudioSessionAction() !== AudioSessionState.START) { throw Error('Audio session cannot be ended because it has not been started'); } this.connection.setAudioSessionAction(AudioSessionState.END); await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendAudioSessionEnd(true); } async sendTTSPlaybackMute(isMuted) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendTTSPlaybackMute(isMuted); } async sendCancelResponse(cancelResponses) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendCancelResponse(cancelResponses); } async sendNarratedAction(text) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendNarratedAction(text); } async sendPerceivedLatenctReport(props) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendPerceivedLatenctReport(props); } async reloadScene() { // TODO: Remove this deprecation warning in the next major release. console.warn('Reload scene is deprecated. Please use changeScene instead.'); await this.changeScene(this.connection.getSceneName()); } async changeScene(name, props) { var _a; if (!sceneHasValidFormat(name) && !characterHasValidFormat(name)) { throw Error(SCENE_HAS_INVALID_FORMAT); } // Clear all conversations if (name !== this.connection.getSceneName()) { const id = (_a = this.oneToOneConversation) === null || _a === void 0 ? void 0 : _a.getConversationId(); const existingConversation = this.connection.conversations.get(id); if (existingConversation) { this.connection.conversations.delete(id); } this.oneToOneConversation = undefined; this.oneToOneConversationIntializeState = ConversationIntializeState.INACTIVE; } return this.connection.change(name, props); } async addCharacters(names) { const invalid = names.find((name) => !characterHasValidFormat(name)); if (invalid) { throw Error(CHARACTER_HAS_INVALID_FORMAT); } const result = await this.connection.send(() => this.connection.getEventFactory().loadCharacters(names)); await this.resolveInterval(() => { const found = this.connection.getCharactersByResourceNames(names); return found.length && found.length === names.length; }); return result; } async removeCharacters(names) { const invalid = names.find((name) => !characterHasValidFormat(name)); if (invalid) { throw Error(CHARACTER_HAS_INVALID_FORMAT); } const ids = (await this.getCharacters()) .filter((c) => names.includes(c.resourceName)) .map((c) => c.id); const result = await this.connection.send(() => this.connection.getEventFactory().unloadCharacters(ids)); this.connection.removeCharacters(names); this.connection.conversations.forEach((conversation) => { conversation.service.changeParticipants(conversation.service .getCharacters() .filter((c) => !names.includes(c.resourceName)) .map((c) => c.resourceName)); }); return result; } async sendCustomPacket(getPacket) { await this.ensureOneToOneConversation(); return this.oneToOneConversation.sendCustomPacket(getPacket); } async interrupt() { return this.connection.interrupt(); } startConversation(participants) { const service = new ConversationService(this.connection, { participants, addCharacters: this.addCharacters.bind(this), startRecording: this.recorder.start.bind(this.recorder), stopRecording: this.recorder.stop.bind(this.recorder), }); this.connection.conversations.set(service.getConversationId(), { service, state: ConversationState.INACTIVE, }); return service; } baseProtoPacket(props) { return this.connection.getEventFactory().baseProtoPacket(props); } markPacketAsHandled(packet) { return this.connection.markPacketAsHandled(packet); } async ensureOneToOneConversation() { if (this.oneToOneConversationIntializeState === ConversationIntializeState.INACTIVE) { this.oneToOneConversationIntializeState = ConversationIntializeState.PROCESSING; const character = await this.getCurrentCharacter(); if (!character) { throw Error(CURRENT_CHARACTER_NOT_SET); } this.oneToOneConversation = new ConversationService(this.connection, { participants: [character.resourceName, ConversationParticipant.USER], addCharacters: this.addCharacters.bind(this), startRecording: this.recorder.start.bind(this.recorder), stopRecording: this.recorder.stop.bind(this.recorder), }); this.addConversationToConnection(this.oneToOneConversation); this.oneToOneConversationIntializeState = ConversationIntializeState.ACTIVE; } else { return new Promise((resolve) => { const interval = setInterval(() => { if (this.oneToOneConversationIntializeState === ConversationIntializeState.ACTIVE) { clearInterval(interval); this.connection.removeInterval(interval); resolve(); } }, 10); this.connection.addInterval(interval); }); } } async resolveInterval(done) { return new Promise((resolve) => { const interval = setInterval(() => { if (done()) { clearInterval(interval); this.connection.removeInterval(interval); resolve(); } }, 10); this.connection.addInterval(interval); }); } addConversationToConnection(conversation) { if (!this.connection.conversations.has(conversation.getConversationId())) { this.connection.conversations.set(conversation.getConversationId(), { service: conversation, state: ConversationState.INACTIVE, }); } } async recorderListener(base64AudioChunk) { const conversation = this.connection.getCurrentAudioConversation(); if (!conversation) { this.connection.onError(new InworldError('No conversation is available to send audio.')); return; } if (!this.connection.isActive() && this.connection.getAudioSessionAction() !== AudioSessionState.START) { await conversation.sendAudioSessionStart(); } conversation.sendAudio(base64AudioChunk); } }