From 17be7e207fbf04d0716613bd14505648d3bac3aa Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 13:18:29 +0300 Subject: [PATCH 1/9] OV-230: + add speech api --- frontend/src/bundles/studio/enums/enums.ts | 2 +- frontend/src/bundles/studio/speech-api.ts | 40 +++++++++++++++++++ frontend/src/bundles/studio/studio.ts | 9 ++++- frontend/src/bundles/studio/types/types.ts | 4 ++ frontend/src/framework/store/store.package.ts | 4 +- 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 frontend/src/bundles/studio/speech-api.ts diff --git a/frontend/src/bundles/studio/enums/enums.ts b/frontend/src/bundles/studio/enums/enums.ts index 72da1dd67..907a8b417 100644 --- a/frontend/src/bundles/studio/enums/enums.ts +++ b/frontend/src/bundles/studio/enums/enums.ts @@ -1,2 +1,2 @@ export { RowNames } from './row-names.enum.js'; -export { AvatarsApiPath } from 'shared'; +export { AvatarsApiPath, SpeechApiPath } from 'shared'; diff --git a/frontend/src/bundles/studio/speech-api.ts b/frontend/src/bundles/studio/speech-api.ts new file mode 100644 index 000000000..852725f89 --- /dev/null +++ b/frontend/src/bundles/studio/speech-api.ts @@ -0,0 +1,40 @@ +import { ApiPath, ContentType } from '~/bundles/common/enums/enums.js'; +import { + type GenerateSpeechRequestDto, + type GenerateSpeechResponseDto, +} from '~/bundles/studio/types/types.js'; +import { type Http, HTTPMethod } from '~/framework/http/http.js'; +import { BaseHttpApi } from '~/framework/http-api/http-api.js'; +import { type Storage } from '~/framework/storage/storage.js'; + +import { SpeechApiPath } from './enums/enums.js'; + +type Constructor = { + baseUrl: string; + http: Http; + storage: Storage; +}; + +class SpeechApi extends BaseHttpApi { + public constructor({ baseUrl, http, storage }: Constructor) { + super({ path: ApiPath.SPEECH, baseUrl, http, storage }); + } + + public async generateScriptSpeech( + payload: GenerateSpeechRequestDto, + ): Promise { + const response = await this.load( + this.getFullEndpoint(SpeechApiPath.GENERATE, {}), + { + method: HTTPMethod.POST, + contentType: ContentType.JSON, + payload: JSON.stringify(payload), + hasAuth: true, + }, + ); + + return await response.json(); + } +} + +export { SpeechApi }; diff --git a/frontend/src/bundles/studio/studio.ts b/frontend/src/bundles/studio/studio.ts index ae6dd9f84..c05836eec 100644 --- a/frontend/src/bundles/studio/studio.ts +++ b/frontend/src/bundles/studio/studio.ts @@ -3,6 +3,7 @@ import { http } from '~/framework/http/http.js'; import { storage } from '~/framework/storage/storage.js'; import { AvatarsApi } from './avatars-api.js'; +import { SpeechApi } from './speech-api.js'; const avatarsApi = new AvatarsApi({ baseUrl: config.ENV.API.ORIGIN_URL, @@ -10,4 +11,10 @@ const avatarsApi = new AvatarsApi({ http, }); -export { avatarsApi }; +const speechApi = new SpeechApi({ + baseUrl: config.ENV.API.ORIGIN_URL, + storage, + http, +}); + +export { avatarsApi, speechApi }; diff --git a/frontend/src/bundles/studio/types/types.ts b/frontend/src/bundles/studio/types/types.ts index 59ed88f7a..05f2b10db 100644 --- a/frontend/src/bundles/studio/types/types.ts +++ b/frontend/src/bundles/studio/types/types.ts @@ -10,3 +10,7 @@ export { type TimelineItem, type TimelineItemWithSpan, } from './timeline-item.type.js'; +export { + type GenerateSpeechRequestDto, + type GenerateSpeechResponseDto, +} from 'shared'; diff --git a/frontend/src/framework/store/store.package.ts b/frontend/src/framework/store/store.package.ts index 2b68508d1..3cc3e0149 100644 --- a/frontend/src/framework/store/store.package.ts +++ b/frontend/src/framework/store/store.package.ts @@ -9,7 +9,7 @@ import { authApi } from '~/bundles/auth/auth.js'; import { reducer as authReducer } from '~/bundles/auth/store/auth.js'; import { AppEnvironment } from '~/bundles/common/enums/enums.js'; import { reducer as studioReducer } from '~/bundles/studio/store/studio.js'; -import { avatarsApi } from '~/bundles/studio/studio.js'; +import { avatarsApi, speechApi } from '~/bundles/studio/studio.js'; import { userApi } from '~/bundles/users/users.js'; import { type Config } from '~/framework/config/config.js'; import { storage } from '~/framework/storage/storage.js'; @@ -25,6 +25,7 @@ type ExtraArguments = { authApi: typeof authApi; userApi: typeof userApi; avatarsApi: typeof avatarsApi; + speechApi: typeof speechApi; storage: typeof storage; }; @@ -60,6 +61,7 @@ class Store { authApi, userApi, avatarsApi, + speechApi, storage, }; } From 8b7fff2c328acdc777ae9231b43123e22ce0e957 Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 13:56:51 +0300 Subject: [PATCH 2/9] OV-230: + voiceName to script --- frontend/src/bundles/studio/store/slice.ts | 4 ++++ frontend/src/bundles/studio/types/script.type.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/frontend/src/bundles/studio/store/slice.ts b/frontend/src/bundles/studio/store/slice.ts index 02d0a5799..7d9368b15 100644 --- a/frontend/src/bundles/studio/store/slice.ts +++ b/frontend/src/bundles/studio/store/slice.ts @@ -45,6 +45,9 @@ type DestinationPointerActionPayload = ItemActionPayload & { type: RowType; }; +// TODO: remove when we will have voices in store +const defaultVoiceName = 'en-US-BrianMultilingualNeural'; + type State = { avatars: { dataStatus: ValueOf; @@ -82,6 +85,7 @@ const { reducer, actions, name } = createSlice({ id: uuidv4(), duration: MIN_SCRIPT_DURATION, text: action.payload, + voiceName: defaultVoiceName, }; state.scripts.push(script); diff --git a/frontend/src/bundles/studio/types/script.type.ts b/frontend/src/bundles/studio/types/script.type.ts index 51c0e9a56..0249959c8 100644 --- a/frontend/src/bundles/studio/types/script.type.ts +++ b/frontend/src/bundles/studio/types/script.type.ts @@ -2,6 +2,7 @@ type Script = { id: string; duration: number; text: string; + voiceName: string; url?: string; }; From 4c48f74ea7094c7ddac537d838cffa3af3d5af91 Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 14:23:31 +0300 Subject: [PATCH 3/9] OV-230: + scriptId to endpoint payload --- backend/src/bundles/speech/speech.controller.ts | 4 ++++ backend/src/common/services/azure-ai/azure-ai.service.ts | 3 ++- .../speech/enums/speech-validation-message.enum.ts | 2 ++ .../speech/types/generate-speech-request-dto.type.ts | 1 + .../speech/types/generate-speech-response-dto.type.ts | 1 + .../generate-speech.validation-schema.ts | 8 ++++++++ 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/src/bundles/speech/speech.controller.ts b/backend/src/bundles/speech/speech.controller.ts index 41426ac05..275eb91f5 100644 --- a/backend/src/bundles/speech/speech.controller.ts +++ b/backend/src/bundles/speech/speech.controller.ts @@ -105,6 +105,8 @@ class SpeechController extends BaseController { * type: object * required: [text, voiceName] * properties: + * scriptId: + * type: string * text: * type: string * voiceName: @@ -117,6 +119,8 @@ class SpeechController extends BaseController { * schema: * type: object * properties: + * scriptId: + * type: string * audioUrl: * type: string */ diff --git a/backend/src/common/services/azure-ai/azure-ai.service.ts b/backend/src/common/services/azure-ai/azure-ai.service.ts index a66dde13a..36ac62412 100644 --- a/backend/src/common/services/azure-ai/azure-ai.service.ts +++ b/backend/src/common/services/azure-ai/azure-ai.service.ts @@ -89,6 +89,7 @@ class AzureAIService { } public async textToSpeech({ + scriptId, text, voiceName, }: GenerateSpeechRequestDto): Promise { @@ -106,7 +107,7 @@ class AzureAIService { await this.fileService.uploadFile(audioBuffer, audioFileName); const audioUrl = this.fileService.getCloudFrontFileUrl(audioFileName); - return { audioUrl }; + return { scriptId, audioUrl }; } } diff --git a/shared/src/bundles/speech/enums/speech-validation-message.enum.ts b/shared/src/bundles/speech/enums/speech-validation-message.enum.ts index ce5493a22..4062636f8 100644 --- a/shared/src/bundles/speech/enums/speech-validation-message.enum.ts +++ b/shared/src/bundles/speech/enums/speech-validation-message.enum.ts @@ -1,4 +1,6 @@ const SpeechValidationMessage = { + SCRIPT_ID_REQUIRED: 'Script id is required', + SCRIPT_ID_INVALID: 'Please provide a valid script id', TEXT_REQUIRED: 'Text is required', VOICE_NAME_REQUIRED: 'Voice name is required', VOICE_NAME_INVALID: 'Please enter a valid voice name', diff --git a/shared/src/bundles/speech/types/generate-speech-request-dto.type.ts b/shared/src/bundles/speech/types/generate-speech-request-dto.type.ts index 934279276..d8670f065 100644 --- a/shared/src/bundles/speech/types/generate-speech-request-dto.type.ts +++ b/shared/src/bundles/speech/types/generate-speech-request-dto.type.ts @@ -1,4 +1,5 @@ type GenerateSpeechRequestDto = { + scriptId: string; text: string; voiceName: string; }; diff --git a/shared/src/bundles/speech/types/generate-speech-response-dto.type.ts b/shared/src/bundles/speech/types/generate-speech-response-dto.type.ts index eac31c0ec..e89298a69 100644 --- a/shared/src/bundles/speech/types/generate-speech-response-dto.type.ts +++ b/shared/src/bundles/speech/types/generate-speech-response-dto.type.ts @@ -1,4 +1,5 @@ type GenerateSpeechResponseDto = { + scriptId: string; audioUrl: string; }; diff --git a/shared/src/bundles/speech/validation-schemas/generate-speech.validation-schema.ts b/shared/src/bundles/speech/validation-schemas/generate-speech.validation-schema.ts index 91ade9747..98ec20ca0 100644 --- a/shared/src/bundles/speech/validation-schemas/generate-speech.validation-schema.ts +++ b/shared/src/bundles/speech/validation-schemas/generate-speech.validation-schema.ts @@ -3,12 +3,20 @@ import { z } from 'zod'; import { SpeechValidationMessage } from '../enums/enums.js'; type GenerateSpeechRequestValidationDto = { + scriptId: z.ZodString; text: z.ZodString; voiceName: z.ZodString; }; const generateSpeech = z .object({ + scriptId: z + .string() + .trim() + .min(1, { + message: SpeechValidationMessage.SCRIPT_ID_REQUIRED, + }) + .uuid({ message: SpeechValidationMessage.SCRIPT_ID_INVALID }), text: z.string().trim().min(1, { message: SpeechValidationMessage.TEXT_REQUIRED, }), From 5c7aae40680f8157affb1e9d1f41eed3c65f7fe4 Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 14:41:15 +0300 Subject: [PATCH 4/9] OV-230: + generateScriptSpeech action --- .../script-content/components/script.tsx | 18 +++++++++--------- frontend/src/bundles/studio/store/actions.ts | 18 ++++++++++++++++-- frontend/src/bundles/studio/store/slice.ts | 9 ++++++++- frontend/src/bundles/studio/store/studio.ts | 3 ++- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx index 8f6465e8a..dbb050136 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx @@ -21,12 +21,9 @@ import { AudioPlayer } from '~/bundles/studio/components/audio-player/audio-play import { actions as studioActions } from '~/bundles/studio/store/studio.js'; import { type Script as ScriptT } from '~/bundles/studio/types/types.js'; -// TODO: remove mocked url when script audioUrl will be taken from text-to-speech -const audioUrl = 'https://d2tm5q3cg1nlwf.cloudfront.net/tts_1725818217391.wav'; - type Properties = ScriptT; -const Script: React.FC = ({ id, text, url }) => { +const Script: React.FC = ({ id, text, voiceName, url }) => { const dispatch = useAppDispatch(); const [isPlaying, setIsPlaying] = useState(false); @@ -64,11 +61,14 @@ const Script: React.FC = ({ id, text, url }) => { setIsAudioLoading(true); - //TODO: replace with fetching real script audioUrl - setTimeout(() => { - void dispatch(studioActions.editScript({ id, url: audioUrl })); - }, 1000); - }, [dispatch, id, url]); + void dispatch( + studioActions.generateScriptSpeech({ + scriptId: id, + text, + voiceName, + }), + ); + }, [dispatch, id, text, url, voiceName]); const handleAudioEnd = useCallback((): void => { setIsPlaying(false); diff --git a/frontend/src/bundles/studio/store/actions.ts b/frontend/src/bundles/studio/store/actions.ts index 53953bcd2..dcf1572e1 100644 --- a/frontend/src/bundles/studio/store/actions.ts +++ b/frontend/src/bundles/studio/store/actions.ts @@ -1,7 +1,11 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; -import { type AvatarGetAllResponseDto } from '~/bundles/studio/types/types.js'; +import { + type AvatarGetAllResponseDto, + type GenerateSpeechRequestDto, + type GenerateSpeechResponseDto, +} from '~/bundles/studio/types/types.js'; import { name as sliceName } from './slice.js'; @@ -15,4 +19,14 @@ const loadAvatars = createAsyncThunk< return avatarsApi.loadAvatars(); }); -export { loadAvatars }; +const generateScriptSpeech = createAsyncThunk< + GenerateSpeechResponseDto, + GenerateSpeechRequestDto, + AsyncThunkConfig +>(`${sliceName}/generate-script-speech`, (payload, { extra }) => { + const { speechApi } = extra; + + return speechApi.generateScriptSpeech(payload); +}); + +export { generateScriptSpeech, loadAvatars }; diff --git a/frontend/src/bundles/studio/store/slice.ts b/frontend/src/bundles/studio/store/slice.ts index 7d9368b15..7dcbd6150 100644 --- a/frontend/src/bundles/studio/store/slice.ts +++ b/frontend/src/bundles/studio/store/slice.ts @@ -29,7 +29,7 @@ import { type Script, type TimelineItemWithSpan, } from '../types/types.js'; -import { loadAvatars } from './actions.js'; +import { generateScriptSpeech, loadAvatars } from './actions.js'; type SelectedItem = { id: string; @@ -241,6 +241,13 @@ const { reducer, actions, name } = createSlice({ state.avatars.items = []; state.avatars.dataStatus = DataStatus.REJECTED; }); + builder.addCase(generateScriptSpeech.fulfilled, (state, action) => { + const { scriptId, audioUrl } = action.payload; + + state.scripts = state.scripts.map((script) => + script.id === scriptId ? { ...script, url: audioUrl } : script, + ); + }); }, }); diff --git a/frontend/src/bundles/studio/store/studio.ts b/frontend/src/bundles/studio/store/studio.ts index 696555ecc..25ad14858 100644 --- a/frontend/src/bundles/studio/store/studio.ts +++ b/frontend/src/bundles/studio/store/studio.ts @@ -1,9 +1,10 @@ -import { loadAvatars } from './actions.js'; +import { generateScriptSpeech, loadAvatars } from './actions.js'; import { actions } from './slice.js'; const allActions = { ...actions, loadAvatars, + generateScriptSpeech, }; export { allActions as actions }; From 12f9de9a9a50c45e796b2efc078ee01328c4b31e Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 14:55:17 +0300 Subject: [PATCH 5/9] OV-230: + status to script --- .../script-content/components/script.tsx | 17 ++++---------- frontend/src/bundles/studio/store/slice.ts | 23 ++++++++++++++++++- .../src/bundles/studio/types/script.type.ts | 4 ++++ 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx index dbb050136..57cb72043 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx @@ -9,10 +9,10 @@ import { Tooltip, VStack, } from '~/bundles/common/components/components.js'; +import { DataStatus } from '~/bundles/common/enums/enums.js'; import { useAppDispatch, useCallback, - useEffect, useMemo, useState, } from '~/bundles/common/hooks/hooks.js'; @@ -23,11 +23,10 @@ import { type Script as ScriptT } from '~/bundles/studio/types/types.js'; type Properties = ScriptT; -const Script: React.FC = ({ id, text, voiceName, url }) => { +const Script: React.FC = ({ id, status, text, voiceName, url }) => { const dispatch = useAppDispatch(); const [isPlaying, setIsPlaying] = useState(false); - const [isAudioLoading, setIsAudioLoading] = useState(false); const handleDeleteScript = useCallback((): void => { void dispatch(studioActions.deleteScript(id)); @@ -59,8 +58,6 @@ const Script: React.FC = ({ id, text, voiceName, url }) => { return; } - setIsAudioLoading(true); - void dispatch( studioActions.generateScriptSpeech({ scriptId: id, @@ -74,19 +71,13 @@ const Script: React.FC = ({ id, text, voiceName, url }) => { setIsPlaying(false); }, []); - useEffect(() => { - if (url) { - setIsAudioLoading(false); - } - }, [url]); - const iconComponent = useMemo(() => { - if (isAudioLoading) { + if (status === DataStatus.PENDING) { return Spinner; } return isPlaying ? IconName.STOP : IconName.PLAY; - }, [isAudioLoading, isPlaying]); + }, [isPlaying, status]); return ( diff --git a/frontend/src/bundles/studio/store/slice.ts b/frontend/src/bundles/studio/store/slice.ts index 7dcbd6150..cd156407e 100644 --- a/frontend/src/bundles/studio/store/slice.ts +++ b/frontend/src/bundles/studio/store/slice.ts @@ -86,6 +86,7 @@ const { reducer, actions, name } = createSlice({ duration: MIN_SCRIPT_DURATION, text: action.payload, voiceName: defaultVoiceName, + status: DataStatus.IDLE, }; state.scripts.push(script); @@ -241,11 +242,31 @@ const { reducer, actions, name } = createSlice({ state.avatars.items = []; state.avatars.dataStatus = DataStatus.REJECTED; }); + builder.addCase(generateScriptSpeech.pending, (state, action) => { + const { scriptId } = action.meta.arg; + + state.scripts = state.scripts.map((script) => + script.id === scriptId + ? { ...script, status: DataStatus.PENDING } + : script, + ); + }); builder.addCase(generateScriptSpeech.fulfilled, (state, action) => { const { scriptId, audioUrl } = action.payload; state.scripts = state.scripts.map((script) => - script.id === scriptId ? { ...script, url: audioUrl } : script, + script.id === scriptId + ? { ...script, url: audioUrl, status: DataStatus.FULFILLED } + : script, + ); + }); + builder.addCase(generateScriptSpeech.rejected, (state, action) => { + const { scriptId } = action.meta.arg; + + state.scripts = state.scripts.map((script) => + script.id === scriptId + ? { ...script, status: DataStatus.REJECTED } + : script, ); }); }, diff --git a/frontend/src/bundles/studio/types/script.type.ts b/frontend/src/bundles/studio/types/script.type.ts index 0249959c8..60be5c60c 100644 --- a/frontend/src/bundles/studio/types/script.type.ts +++ b/frontend/src/bundles/studio/types/script.type.ts @@ -1,8 +1,12 @@ +import { type DataStatus } from '~/bundles/common/enums/enums.js'; +import { type ValueOf } from '~/bundles/common/types/types.js'; + type Script = { id: string; duration: number; text: string; voiceName: string; + status: ValueOf; url?: string; }; From e83dafaef4d4361737df5994531787b874e66990 Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 15:02:08 +0300 Subject: [PATCH 6/9] OV-230: * rename hooks --- .../components/audio-player/audio-player.tsx | 18 +++++++++--------- .../script-content/components/script.tsx | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/bundles/studio/components/audio-player/audio-player.tsx b/frontend/src/bundles/studio/components/audio-player/audio-player.tsx index 7edaf7098..1108b3002 100644 --- a/frontend/src/bundles/studio/components/audio-player/audio-player.tsx +++ b/frontend/src/bundles/studio/components/audio-player/audio-player.tsx @@ -13,15 +13,15 @@ import { AudioEvent } from './enums/enums.js'; type Properties = { isPlaying: boolean; audioUrl: string; - handleAudioEnd: () => void; - handleSetDuration: (duration: number) => void; + onAudioEnd: () => void; + onSetDuration: (duration: number) => void; }; const AudioPlayer: React.FC = ({ isPlaying, audioUrl, - handleAudioEnd, - handleSetDuration, + onAudioEnd, + onSetDuration, }) => { const playerReference = useRef(null); @@ -39,22 +39,22 @@ const AudioPlayer: React.FC = ({ getAudioData(audioUrl) .then(({ durationInSeconds }) => { setDurationInFrames(Math.round(durationInSeconds * FPS)); - handleSetDuration(durationInSeconds); + onSetDuration(durationInSeconds); }) .catch(() => { setDurationInFrames(1); }); - }, [audioUrl, handleSetDuration]); + }, [audioUrl, onSetDuration]); useEffect(() => { const player = playerReference.current; - player?.addEventListener(AudioEvent.ENDED, handleAudioEnd); + player?.addEventListener(AudioEvent.ENDED, onAudioEnd); return () => { - player?.removeEventListener(AudioEvent.ENDED, handleAudioEnd); + player?.removeEventListener(AudioEvent.ENDED, onAudioEnd); }; - }, [handleAudioEnd, playerReference]); + }, [onAudioEnd, playerReference]); return ( = ({ id, status, text, voiceName, url }) => { )} From 9abb616ed37d0866c9e9cc12a866c7435f152525 Mon Sep 17 00:00:00 2001 From: Oleksandra Nedashkivska Date: Tue, 10 Sep 2024 18:22:00 +0300 Subject: [PATCH 7/9] OV-230: * replace studio dataStatus to root --- .../avatars-content/avatars-content.tsx | 7 +++--- frontend/src/bundles/studio/store/slice.ts | 22 ++++++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/avatars-content/avatars-content.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/avatars-content/avatars-content.tsx index d6d519745..87dc98853 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/avatars-content/avatars-content.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/avatars-content/avatars-content.tsx @@ -18,9 +18,10 @@ import { AvatarCard } from './components/components.js'; const AvatarsContent: React.FC = () => { const dispatch = useAppDispatch(); - const { items: avatars, dataStatus } = useAppSelector( - ({ studio }) => studio.avatars, - ); + const { avatars, dataStatus } = useAppSelector(({ studio }) => ({ + avatars: studio.avatars, + dataStatus: studio.dataStatus, + })); useEffect(() => { void dispatch(studioActions.loadAvatars()); diff --git a/frontend/src/bundles/studio/store/slice.ts b/frontend/src/bundles/studio/store/slice.ts index cd156407e..7790fee19 100644 --- a/frontend/src/bundles/studio/store/slice.ts +++ b/frontend/src/bundles/studio/store/slice.ts @@ -49,10 +49,8 @@ type DestinationPointerActionPayload = ItemActionPayload & { const defaultVoiceName = 'en-US-BrianMultilingualNeural'; type State = { - avatars: { - dataStatus: ValueOf; - items: Array | []; - }; + dataStatus: ValueOf; + avatars: Array | []; scenes: Array; scripts: Array