Skip to content

Commit

Permalink
Merge pull request #231 from BinaryStudioAcademy/task/OV-173-add-azur…
Browse files Browse the repository at this point in the history
…e-avatar-integration

OV-173: add azure avatar integration
  • Loading branch information
nikita-remeslov authored Sep 11, 2024
2 parents 2f4681f + 73060ae commit f3acb75
Show file tree
Hide file tree
Showing 53 changed files with 561 additions and 59 deletions.
89 changes: 89 additions & 0 deletions backend/src/bundles/avatar-videos/avatar-videos.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { type UserGetCurrentResponseDto, ApiPath } from 'shared';

import {
type ApiHandlerOptions,
type ApiHandlerResponse,
} from '~/common/controller/controller.js';
import { BaseController } from '~/common/controller/controller.js';
import { HttpCode, HTTPMethod } from '~/common/http/http.js';
import { type Logger } from '~/common/logger/logger.js';

import { type AvatarVideoService } from './avatar-videos.service.js';
import { AvatarVideosApiPath } from './enums/enums.js';
import { type RenderAvatarVideoRequestDto } from './types/types.js';
import { renderAvatarVideoValidationSchema } from './validation-schemas/validation-schemas.js';

class AvatarVideoController extends BaseController {
private avatarVideoService: AvatarVideoService;

public constructor(logger: Logger, avatarVideoService: AvatarVideoService) {
super(logger, ApiPath.AVATAR_VIDEO);

this.avatarVideoService = avatarVideoService;

this.addRoute({
path: AvatarVideosApiPath.ROOT,
method: HTTPMethod.POST,
validation: {
body: renderAvatarVideoValidationSchema,
},
handler: (options) =>
this.renderAvatarVideo(
options as ApiHandlerOptions<{
body: RenderAvatarVideoRequestDto;
}>,
),
});
}

/**
* @swagger
* /avatar-video:
* post:
* description: Generate video from text
* security:
* - bearerAuth: []
* requestBody:
* description: Data for video generation
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [text, voice, avatarName, avatarStyle]
* properties:
* text:
* type: string
* voice:
* type: string
* avatarName:
* type: string
* avatarStyle:
* type: string
* responses:
* 201:
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: string
*/
private async renderAvatarVideo(
options: ApiHandlerOptions<{
body: RenderAvatarVideoRequestDto;
}>,
): Promise<ApiHandlerResponse> {
return {
payload: await this.avatarVideoService.renderAvatarVideo({
...options.body,
userId: (options.user as UserGetCurrentResponseDto).id,
}),
status: HttpCode.CREATED,
};
}
}

export { AvatarVideoController };
130 changes: 130 additions & 0 deletions backend/src/bundles/avatar-videos/avatar-videos.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { HttpCode, HttpError } from 'shared';
import { v4 as uuidv4 } from 'uuid';

import { type AzureAIService } from '~/common/services/azure-ai/azure-ai.service.js';
import { type FileService } from '~/common/services/file/file.service.js';

import { type VideoService } from '../videos/video.service.js';
import { REQUEST_DELAY } from './constants/constnats.js';
import {
GenerateAvatarResponseStatus,
RenderVideoErrorMessage,
} from './enums/enums.js';
import { getFileName } from './helpers/helpers.js';
import {
type RenderAvatarResponseDto,
type RenderAvatarVideoRequestDto,
} from './types/types.js';

type HandleRenderVideoArguments = {
id: string;
userId: string;
url: string;
};

class AvatarVideoService {
private azureAIService: AzureAIService;
private fileService: FileService;
private videoService: VideoService;

public constructor(
azureAIService: AzureAIService,
fileService: FileService,
videoService: VideoService,
) {
this.azureAIService = azureAIService;
this.fileService = fileService;
this.videoService = videoService;
}

private async saveAvatarVideo(url: string, id: string): Promise<string> {
const buffer = await this.azureAIService.getAvatarVideoBuffer(url);

const fileName = getFileName(id);

await this.fileService.uploadFile(buffer, fileName);
return this.fileService.getCloudFrontFileUrl(fileName);
}

public async renderAvatarVideo(
payload: RenderAvatarVideoRequestDto & { userId: string },
): Promise<RenderAvatarResponseDto> {
const { userId, ...avatarConfig } = payload;
const response = await this.azureAIService.renderAvatarVideo({
id: uuidv4(),
payload: avatarConfig,
});

this.checkAvatarProcessing(response.id, userId);

return response;
}

private checkAvatarProcessing(id: string, userId: string): void {
const interval = setInterval((): void => {
this.azureAIService
.getAvatarVideo(id)
.then((response) => {
if (
response.status ===
GenerateAvatarResponseStatus.SUCCEEDED
) {
this.handleSuccessfulAvatarGeneration({
id,
userId,
url: response.outputs.result,
})
.then(() => {
// TODO: NOTIFY USER
})
.catch((error) => {
throw new HttpError({
message: error.message,
status: error.status,
});
})
.finally(() => {
clearInterval(interval);
});
} else if (
response.status === GenerateAvatarResponseStatus.FAILED
) {
// TODO: NOTIFY USER
clearInterval(interval);
}
})
.catch((error) => {
clearInterval(interval);
throw new HttpError({
message: error.message,
status: error.status,
});
});
}, REQUEST_DELAY);
}

private async handleSuccessfulAvatarGeneration({
id,
url,
userId,
}: HandleRenderVideoArguments): Promise<void> {
const savedUrl = await this.saveAvatarVideo(url, id);

const videoData = await this.videoService.create({
name: getFileName(id),
url: savedUrl,
userId,
});

if (!videoData) {
throw new HttpError({
message: RenderVideoErrorMessage.NOT_SAVED,
status: HttpCode.BAD_REQUEST,
});
}

await this.azureAIService.removeAvatarVideo(id);
}
}

export { AvatarVideoService };
19 changes: 19 additions & 0 deletions backend/src/bundles/avatar-videos/avatar-videos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { logger } from '~/common/logger/logger.js';
import { azureAIService, fileService } from '~/common/services/services.js';

import { videoService } from '../videos/videos.js';
import { AvatarVideoController } from './avatar-videos.controller.js';
import { AvatarVideoService } from './avatar-videos.service.js';

const avatarVideoService = new AvatarVideoService(
azureAIService,
fileService,
videoService,
);

const avatarVideoController = new AvatarVideoController(
logger,
avatarVideoService,
);

export { avatarVideoController };
1 change: 1 addition & 0 deletions backend/src/bundles/avatar-videos/constants/constnats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { REQUEST_DELAY } from './request-delay.constant.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const REQUEST_DELAY = 4000;

export { REQUEST_DELAY };
3 changes: 3 additions & 0 deletions backend/src/bundles/avatar-videos/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { GenerateAvatarResponseStatus } from './generate-avatar-response-status.enum.js';
export { RenderVideoErrorMessage } from './render-video-error-message.enum.js';
export { AvatarVideosApiPath } from 'shared';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const GenerateAvatarResponseStatus = {
SUCCEEDED: 'Succeeded',
FAILED: 'Failed',
} as const;

export { GenerateAvatarResponseStatus };
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const RenderVideoErrorMessage = {
NOT_FOUND: 'Video not found',
NOT_SAVED: 'Video was not saved',
} as const;

export { RenderVideoErrorMessage };
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const getFileName = (id: string): string => {
return `${id}.webm`;
};

export { getFileName };
1 change: 1 addition & 0 deletions backend/src/bundles/avatar-videos/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getFileName } from './get-file-name.helper.js';
4 changes: 4 additions & 0 deletions backend/src/bundles/avatar-videos/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
type RenderAvatarResponseDto,
type RenderAvatarVideoRequestDto,
} from 'shared';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { renderAvatarVideoValidationSchema } from 'shared';
2 changes: 1 addition & 1 deletion backend/src/bundles/videos/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ const videoRepository = new VideoRepository(VideoModel);
const videoService = new VideoService(videoRepository);
const videoController = new VideoController(logger, videoService);

export { videoController };
export { videoController, videoService };
2 changes: 2 additions & 0 deletions backend/src/common/server-application/server-application.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { authController } from '~/bundles/auth/auth.js';
import { avatarVideoController } from '~/bundles/avatar-videos/avatar-videos.js';
import { avatarController } from '~/bundles/avatars/avatars.js';
import { chatController } from '~/bundles/chat/chat.js';
import { speechController } from '~/bundles/speech/speech.js';
Expand All @@ -20,6 +21,7 @@ const apiV1 = new BaseServerAppApi(
...videoController.routes,
...chatController.routes,
...speechController.routes,
...avatarVideoController.routes,
);

const serverApp = new BaseServerApp({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { ApiPath, ContentType, HttpHeader, HTTPMethod } from 'shared';

import { config } from '~/common/config/config.js';
import { BaseHttpApi } from '~/common/http/http.js';
import {
type GetAvatarVideoResponseDto,
type RenderAvatarVideoResponseDto,
} from '~/common/services/azure-ai/types/types.js';

import { API_VERSION } from './constants/constants.js';
import { AvatarApiPath } from './enums/enums.js';
import { type RenderAvatarVideoArgument } from './types/types.js';
import {
type GetAvatarVideoResponseApiDto,
type RenderAvatarVideoApiArgument,
type RenderAvatarVideoApiResponseDto,
} from './types/types.js';

type Constructor = {
baseUrl: string;
Expand All @@ -24,7 +24,7 @@ class AvatarVideoApi extends BaseHttpApi {

public async getAvatarVideo(
id: string,
): Promise<GetAvatarVideoResponseDto> {
): Promise<GetAvatarVideoResponseApiDto> {
const response = await this.load(
this.getFullEndpoint(
`${AvatarApiPath.BATCHSYNTHESES}/${id}?api-version=${API_VERSION}`,
Expand All @@ -42,7 +42,7 @@ class AvatarVideoApi extends BaseHttpApi {
},
);

return await response.json<GetAvatarVideoResponseDto>();
return await response.json<GetAvatarVideoResponseApiDto>();
}

public async deleteAvatarVideo(id: string): Promise<unknown> {
Expand All @@ -67,7 +67,7 @@ class AvatarVideoApi extends BaseHttpApi {
public async renderAvatarVideo({
id,
payload,
}: RenderAvatarVideoArgument): Promise<RenderAvatarVideoResponseDto> {
}: RenderAvatarVideoApiArgument): Promise<RenderAvatarVideoApiResponseDto> {
const response = await this.load(
this.getFullEndpoint(
`${AvatarApiPath.BATCHSYNTHESES}/${id}?api-version=${API_VERSION}`,
Expand All @@ -86,7 +86,17 @@ class AvatarVideoApi extends BaseHttpApi {
},
);

return await response.json<RenderAvatarVideoResponseDto>();
return await response.json<RenderAvatarVideoApiResponseDto>();
}

public async getAvatarVideoBuffer(url: string): Promise<Buffer> {
const response = await this.load(url, {
method: HTTPMethod.GET,
contentType: ContentType.JSON,
});

const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ const avatarVideoApi = new AvatarVideoApi({
});

export { avatarVideoApi };
export { AvatarVideoApi } from './avatar-video-base.js';
export { AvatarApiPath } from './enums/enums.js';
export {
type GetAvatarVideoResponseApiDto,
type RenderAvatarVideoApiRequestDto,
type RenderAvatarVideoApiResponseDto,
type RenderAvatarVideoArgument,
} from './types/types.js';
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { type InputKind } from './input-kind.type.js';
import { type VideoCodec, type VideoFormat } from './types.js';
import { type InputKind, type VideoCodec, type VideoFormat } from './types.js';

type GetAvatarVideoResponseDto = {
type GetAvatarVideoResponseApiDto = {
id: string;
internalId: string;
status: string;
Expand Down Expand Up @@ -34,4 +33,4 @@ type GetAvatarVideoResponseDto = {
};
};

export { type GetAvatarVideoResponseDto };
export { type GetAvatarVideoResponseApiDto };
Loading

0 comments on commit f3acb75

Please sign in to comment.