@inworld/web-core
Version:
325 lines (324 loc) • 14.1 kB
JavaScript
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);
}
}