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