Skip to content

Commit c037d1c

Browse files
Merge pull request #362 from BinaryStudioAcademy/task/OV-322-add-remotion-render
OV-322: add remotion render
2 parents c916093 + 26f60b2 commit c037d1c

28 files changed

+10358
-7041
lines changed

backend/.env.example

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ AWS_SECRET_ACCESS_KEY=see-in-slack
2626
AWS_S3_REGION=eu-north-1
2727
AWS_S3_BUCKET_NAME=bsa-2024-outreachvids
2828
AWS_CLOUDFRONT_DOMAIN_ID=d2tm5q3cg1nlwf
29+
AWS_CLOUDFRONT_DOMAIN_ID_FOR_RENDERED_VIDEO=SOME_SECRET_KEY
2930

3031
#
3132
# OPEN AI
@@ -48,3 +49,10 @@ ORIGIN=http://localhost:3000
4849
AZURE_SUBSCRIPTION_KEY=see-in-slack
4950
AZURE_SERVICE_REGION=see-in-slack
5051
AZURE_SERVICE_ENDPOINT=see-in-slack
52+
53+
#
54+
# REMOTION
55+
#
56+
REMOTION_LAMBDA_FUNCTION_NAME=SOME_SECRET_KEY
57+
REMOTION_SERVE_URL=SOME_SECRET_KEY
58+
REMOTION_BUCKET_NAME=SOME_SECRET_KEY

backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@fastify/static": "7.0.4",
3939
"@fastify/swagger": "8.15.0",
4040
"@fastify/swagger-ui": "4.0.1",
41+
"@remotion/lambda": "4.0.201",
4142
"bcrypt": "5.1.1",
4243
"convict": "6.2.4",
4344
"dotenv": "16.4.5",

backend/src/bundles/avatar-videos/avatar-videos.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class AvatarVideoController extends BaseController {
7777
}>,
7878
): Promise<ApiHandlerResponse> {
7979
const userId = (options.user as UserGetCurrentResponseDto).id;
80+
8081
const videoRecord = await this.avatarVideoService.createVideo({
8182
...options.body,
8283
userId,
@@ -88,7 +89,6 @@ class AvatarVideoController extends BaseController {
8889

8990
await this.avatarVideoService.submitAvatarsConfigs(
9091
avatarsConfigs,
91-
userId,
9292
videoRecord.id,
9393
);
9494

backend/src/bundles/avatar-videos/avatar-videos.service.ts

+88-32
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,50 @@ import { v4 as uuidv4 } from 'uuid';
55
import { type AvatarData } from '~/common/services/azure-ai/avatar-video/types/avatar-data.js';
66
import { type AzureAIService } from '~/common/services/azure-ai/azure-ai.service.js';
77
import { type FileService } from '~/common/services/file/file.service.js';
8+
import { type RemotionService } from '~/common/services/remotion/remotion.service.js';
9+
import { type RemotionAvatarScene } from '~/common/services/remotion/type/types.js';
810

911
import { type VideoService } from '../videos/video.service.js';
1012
import { REQUEST_DELAY } from './constants/constnats.js';
1113
import {
1214
GenerateAvatarResponseStatus,
1315
RenderVideoErrorMessage,
1416
} from './enums/enums.js';
15-
import { distributeScriptsToScenes, getFileName } from './helpers/helpers.js';
17+
import { generatedAvatarToRemotionScene } from './helpers/generated-avatars-to-remotion-scenes.helper.js';
18+
import {
19+
distributeScriptsToScenes,
20+
getFileName,
21+
getTotalDuration,
22+
} from './helpers/helpers.js';
1623
import {
1724
type Composition,
25+
type GeneratedAvatarData,
1826
type RenderAvatarVideoRequestDto,
1927
} from './types/types.js';
2028

21-
type HandleRenderVideoArguments = {
22-
videoRecordId: string;
23-
avatars: {
24-
id: string;
25-
url: string;
26-
}[];
29+
type Constructor = {
30+
azureAIService: AzureAIService;
31+
fileService: FileService;
32+
videoService: VideoService;
33+
remotionService: RemotionService;
2734
};
2835

2936
class AvatarVideoService {
3037
private azureAIService: AzureAIService;
3138
private fileService: FileService;
3239
private videoService: VideoService;
40+
private remotionService: RemotionService;
3341

34-
public constructor(
35-
azureAIService: AzureAIService,
36-
fileService: FileService,
37-
videoService: VideoService,
38-
) {
42+
public constructor({
43+
azureAIService,
44+
fileService,
45+
remotionService,
46+
videoService,
47+
}: Constructor) {
3948
this.azureAIService = azureAIService;
4049
this.fileService = fileService;
4150
this.videoService = videoService;
51+
this.remotionService = remotionService;
4252
}
4353

4454
private async saveAvatarVideo(url: string, id: string): Promise<string> {
@@ -70,7 +80,6 @@ class AvatarVideoService {
7080

7181
public async submitAvatarsConfigs(
7282
configs: AvatarData[],
73-
userId: string,
7483
recordId: string,
7584
): Promise<string[]> {
7685
try {
@@ -87,7 +96,7 @@ class AvatarVideoService {
8796
return response.id;
8897
});
8998

90-
this.checkAvatarsProcessing(ids, userId, recordId).catch(() => {
99+
this.checkAvatarsProcessing(ids, recordId).catch(() => {
91100
throw new HttpError({
92101
message: RenderVideoErrorMessage.RENDER_ERROR,
93102
status: HTTPCode.BAD_REQUEST,
@@ -105,7 +114,6 @@ class AvatarVideoService {
105114

106115
public async checkAvatarsProcessing(
107116
ids: string[],
108-
userId: string,
109117
videoRecordId: string,
110118
): Promise<void> {
111119
try {
@@ -116,7 +124,7 @@ class AvatarVideoService {
116124
);
117125

118126
await this.handleSuccessfulAvatarsGeneration({
119-
avatars: response,
127+
generatedAvatars: response,
120128
videoRecordId,
121129
});
122130
} catch {
@@ -127,9 +135,7 @@ class AvatarVideoService {
127135
}
128136
}
129137

130-
private checkAvatarStatus(
131-
id: string,
132-
): Promise<{ id: string; url: string }> {
138+
private checkAvatarStatus(id: string): Promise<GeneratedAvatarData> {
133139
return new Promise((resolve, reject) => {
134140
const interval = setInterval(() => {
135141
this.azureAIService
@@ -140,7 +146,12 @@ class AvatarVideoService {
140146
GenerateAvatarResponseStatus.SUCCEEDED
141147
) {
142148
clearInterval(interval);
143-
resolve({ id, url: response.outputs.result });
149+
resolve({
150+
id,
151+
url: response.outputs.result,
152+
durationInMilliseconds:
153+
response.properties.durationInMilliseconds,
154+
});
144155
} else if (
145156
response.status ===
146157
GenerateAvatarResponseStatus.FAILED
@@ -170,21 +181,38 @@ class AvatarVideoService {
170181

171182
private async handleSuccessfulAvatarsGeneration({
172183
videoRecordId,
173-
avatars,
174-
}: HandleRenderVideoArguments): Promise<void> {
175-
// TODO: REPLACE THIS LOGIC WITH RENDER VIDEO
176-
// TODO: NOTIFY USER
177-
const firstAvatarId = avatars[0]?.id;
178-
const url = avatars[0]?.url;
184+
generatedAvatars,
185+
}: {
186+
videoRecordId: string;
187+
generatedAvatars: GeneratedAvatarData[];
188+
}): Promise<void> {
189+
const scenes = generatedAvatarToRemotionScene(generatedAvatars);
190+
const scenesWithSavedAvatars = await this.saveGeneratedAvatar(scenes);
191+
192+
const renderId = await this.remotionService.renderVideo({
193+
scenes: scenesWithSavedAvatars,
194+
totalDurationInFrames: getTotalDuration(scenesWithSavedAvatars),
195+
});
196+
197+
const url =
198+
await this.remotionService.getRemotionRenderProgress(renderId);
179199

180-
if (!firstAvatarId || !url) {
200+
await this.removeGeneratedAvatars(generatedAvatars);
201+
await this.removeAvatarsFromBucket(generatedAvatars);
202+
203+
if (!url) {
181204
return;
182205
}
206+
// TODO: NOTIFY USER
207+
await this.updateVideoRecord(videoRecordId, url);
208+
}
183209

184-
const savedUrl = await this.saveAvatarVideo(url, firstAvatarId);
185-
210+
private async updateVideoRecord(
211+
videoRecordId: string,
212+
videoUrl: string,
213+
): Promise<void> {
186214
const videoData = await this.videoService.update(videoRecordId, {
187-
url: savedUrl,
215+
url: videoUrl,
188216
});
189217

190218
if (!videoData) {
@@ -193,13 +221,41 @@ class AvatarVideoService {
193221
status: HTTPCode.BAD_REQUEST,
194222
});
195223
}
224+
}
196225

197-
await Promise.all(
198-
avatars.map((avatar) => {
226+
private async removeGeneratedAvatars(
227+
generatedAvatars: GeneratedAvatarData[],
228+
): Promise<unknown> {
229+
return Promise.all(
230+
generatedAvatars.map((avatar) => {
199231
return this.azureAIService.removeAvatarVideo(avatar.id);
200232
}),
201233
);
202234
}
235+
236+
private async saveGeneratedAvatar(
237+
generatedAvatars: RemotionAvatarScene[],
238+
): Promise<RemotionAvatarScene[]> {
239+
return Promise.all(
240+
generatedAvatars.map(async (avatar) => {
241+
return {
242+
durationInFrames: avatar.durationInFrames,
243+
id: avatar.id,
244+
url: await this.saveAvatarVideo(avatar.url, avatar.id),
245+
};
246+
}),
247+
);
248+
}
249+
250+
private async removeAvatarsFromBucket(
251+
generatedAvatars: GeneratedAvatarData[],
252+
): Promise<unknown> {
253+
return Promise.all(
254+
generatedAvatars.map((avatar) => {
255+
return this.fileService.deleteFile(getFileName(avatar.id));
256+
}),
257+
);
258+
}
203259
}
204260

205261
export { AvatarVideoService };

backend/src/bundles/avatar-videos/avatar-videos.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { logger } from '~/common/logger/logger.js';
2-
import { azureAIService, fileService } from '~/common/services/services.js';
2+
import {
3+
azureAIService,
4+
fileService,
5+
remotionService,
6+
} from '~/common/services/services.js';
37

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

8-
const avatarVideoService = new AvatarVideoService(
12+
const avatarVideoService = new AvatarVideoService({
913
azureAIService,
1014
fileService,
1115
videoService,
12-
);
16+
remotionService,
17+
});
1318

1419
const avatarVideoController = new AvatarVideoController(
1520
logger,
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export { FPS } from './fps.js';
12
export { REQUEST_DELAY } from './request-delay.constant.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const FPS = 30;
2+
3+
export { FPS };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type RemotionAvatarScene } from '~/common/services/remotion/type/types.js';
2+
3+
import { FPS } from '../constants/fps.js';
4+
import { type GeneratedAvatarData } from '../types/types.js';
5+
6+
const generatedAvatarToRemotionScene = (
7+
generatedAvatars: GeneratedAvatarData[],
8+
): RemotionAvatarScene[] => {
9+
return generatedAvatars.map((avatar) => {
10+
return {
11+
id: avatar.id,
12+
url: avatar.url,
13+
durationInFrames: Math.round(
14+
(avatar.durationInMilliseconds / 1000) * FPS,
15+
),
16+
};
17+
});
18+
};
19+
20+
export { generatedAvatarToRemotionScene };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { type RemotionAvatarScene } from '~/common/services/remotion/type/types.js';
2+
3+
const getTotalDuration = (scenes: RemotionAvatarScene[]): number => {
4+
return scenes.reduce((sum, scene) => sum + scene.durationInFrames, 0);
5+
};
6+
7+
export { getTotalDuration };
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { distributeScriptsToScenes } from './distribute-scripts-to-scenes.helper.js';
22
export { getFileName } from './get-file-name.helper.js';
3+
export { getTotalDuration } from './get-total-duration.helper.js';

0 commit comments

Comments
 (0)