Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…4-outreachvids into task/OV-347-connect-generate-script-with-studio
  • Loading branch information
lfelix3011 committed Sep 25, 2024
2 parents 3f1e1fb + e4aa255 commit f69a23a
Show file tree
Hide file tree
Showing 30 changed files with 416 additions and 217 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,7 @@ class AvatarVideoController extends BaseController {
? this.avatarVideoService.updateVideo({ ...videoPayload, videoId })
: this.avatarVideoService.createVideo({ ...videoPayload, userId }));

const scenesConfigs =
this.avatarVideoService.getScenesConfigs(composition);

await this.avatarVideoService.submitAvatarsConfigs(
scenesConfigs,
videoRecord.id,
);
await this.avatarVideoService.renderVideo(composition, videoRecord.id);

return {
payload: { status: ResponseStatus.SUBMITTED },
Expand Down
248 changes: 55 additions & 193 deletions backend/src/bundles/avatar-videos/avatar-videos.service.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,39 @@
import { type VideoGetAllItemResponseDto } from 'shared';
import { HTTPCode, HttpError } from 'shared';
import { type VideoGetAllItemResponseDto, HTTPCode, HttpError } from 'shared';

import { AvatarVideoEvent } from '~/common/enums/enums.js';
import { type AzureAIService } from '~/common/services/azure-ai/azure-ai.service.js';
import { type FileService } from '~/common/services/file/file.service.js';
import { type RemotionService } from '~/common/services/remotion/remotion.service.js';
import { socketEvent } from '~/common/socket/socket.js';

import { type VideoService } from '../videos/video.service.js';
import { REQUEST_DELAY } from './constants/constnats.js';
import {
GenerateAvatarResponseStatus,
RenderVideoErrorMessage,
} from './enums/enums.js';
import { generatedAvatarToRemotionScene } from './helpers/generated-avatars-to-remotion-scenes.helper.js';
import {
distributeScriptsToScenes,
getFileName,
getTotalDuration,
} from './helpers/helpers.js';
import { RenderVideoErrorMessage } from './enums/enums.js';
import { getTotalDuration } from './helpers/helpers.js';
import { type SceneService } from './scenes.service.js';
import {
type Composition,
type CompositionWithGeneratedAvatars,
type CompositionWithScenesForRenderAvatar,
type RenderAvatarVideoRequestDto,
type SceneForRenderAvatar,
type SceneWithGeneratedAvatar,
} from './types/types.js';

type Constructor = {
azureAIService: AzureAIService;
fileService: FileService;
videoService: VideoService;
remotionService: RemotionService;
scenesService: SceneService;
};

class AvatarVideoService {
private azureAIService: AzureAIService;
private fileService: FileService;
private videoService: VideoService;
private remotionService: RemotionService;
private scenesService: SceneService;

public constructor({
azureAIService,
fileService,
remotionService,
videoService,
scenesService,
}: Constructor) {
this.azureAIService = azureAIService;
this.fileService = fileService;
this.videoService = videoService;
this.remotionService = remotionService;
}

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);
this.scenesService = scenesService;
}

public async createVideo({
Expand Down Expand Up @@ -85,194 +61,80 @@ class AvatarVideoService {
});
}

public getScenesConfigs(composition: Composition): SceneForRenderAvatar[] {
return distributeScriptsToScenes(composition);
}

public async submitAvatarsConfigs(
scenesForRenderAvatar: SceneForRenderAvatar[],
recordId: string,
public async renderVideo(
composition: Composition,
videoRecordId: string,
): Promise<void> {
try {
await Promise.all(
scenesForRenderAvatar.map((scene) => {
return this.azureAIService.renderAvatarVideo({
id: scene.id,
payload: scene.avatar,
});
}),
);
const scenesForAvatarsRendering = this.scenesService.getScenesConfigs({
scenes: composition.scenes,
scripts: composition.scripts,
});

this.checkAvatarsProcessing(scenesForRenderAvatar, recordId).catch(
() => {
throw new HttpError({
message: RenderVideoErrorMessage.RENDER_ERROR,
status: HTTPCode.BAD_REQUEST,
});
},
);
} catch {
await this.scenesService.submitAvatarsConfigs(
scenesForAvatarsRendering,
);

this.handleRenderingVideo(
{
...composition,
scenes: scenesForAvatarsRendering,
},
videoRecordId,
).catch(() => {
throw new HttpError({
message: RenderVideoErrorMessage.RENDER_ERROR,
status: HTTPCode.BAD_REQUEST,
});
}
});
}

public async checkAvatarsProcessing(
scenesForRenderAvatar: SceneForRenderAvatar[],
public async handleRenderingVideo(
composition: CompositionWithScenesForRenderAvatar,
videoRecordId: string,
): Promise<void> {
try {
const response = await Promise.all(
scenesForRenderAvatar.map((scene) => {
return this.checkAvatarStatus(scene);
}),
);

await this.handleSuccessfulAvatarsGeneration({
scenesWithGeneratedAvatars: response,
videoRecordId,
});
} catch {
throw new HttpError({
message: RenderVideoErrorMessage.RENDER_ERROR,
status: HTTPCode.BAD_REQUEST,
});
}
}

private checkAvatarStatus(
scene: SceneForRenderAvatar,
): Promise<SceneWithGeneratedAvatar> {
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
this.azureAIService
.getAvatarVideo(scene.id)
.then((response) => {
if (
response.status ===
GenerateAvatarResponseStatus.SUCCEEDED
) {
clearInterval(interval);

resolve({
...scene,
avatar: {
url: response.outputs.result,
},
durationInMilliseconds:
response.properties.durationInMilliseconds,
});
} else if (
response.status ===
GenerateAvatarResponseStatus.FAILED
) {
reject(
new HttpError({
message:
RenderVideoErrorMessage.RENDER_ERROR,
status: HTTPCode.BAD_REQUEST,
}),
);
clearInterval(interval);
}
})
.catch(() => {
reject(
new HttpError({
message: RenderVideoErrorMessage.RENDER_ERROR,
status: HTTPCode.BAD_REQUEST,
}),
);
clearInterval(interval);
});
}, REQUEST_DELAY);
const scenesWithGeneratedAvatar =
await this.scenesService.checkAvatarsProcessing(composition.scenes);

await this.handleSuccessfulAvatarsGeneration({
videoRecordId,
compositionWithGeneratedAvatars: {
...composition,
scenes: scenesWithGeneratedAvatar,
},
});
}

private async handleSuccessfulAvatarsGeneration({
videoRecordId,
scenesWithGeneratedAvatars,
compositionWithGeneratedAvatars,
}: {
videoRecordId: string;
scenesWithGeneratedAvatars: SceneWithGeneratedAvatar[];
compositionWithGeneratedAvatars: CompositionWithGeneratedAvatars;
}): Promise<void> {
const scenesWithSavedAvatars = await this.saveGeneratedAvatar(
scenesWithGeneratedAvatars,
);
const scenesForRendering = generatedAvatarToRemotionScene(
scenesWithSavedAvatars,
);
const compositionForRender = {
...compositionWithGeneratedAvatars,
scenes: await this.scenesService.getScenesForRemotionRender(
compositionWithGeneratedAvatars.scenes,
),
};

const renderId = await this.remotionService.renderVideo({
scenes: scenesForRendering,
totalDurationInFrames: getTotalDuration(scenesForRendering),
...compositionForRender,
totalDurationInFrames: getTotalDuration(
compositionForRender.scenes,
),
});

const url =
await this.remotionService.getRemotionRenderProgress(renderId);

if (url) {
await this.updateVideoRecord(videoRecordId, url);
// TODO: NOTIFY USER
await this.videoService.update(videoRecordId, { url });
socketEvent.emitNotification(AvatarVideoEvent.RENDER_SUCCESS);
}

await this.removeGeneratedAvatars(scenesWithSavedAvatars);
await this.removeAvatarsFromBucket(scenesWithSavedAvatars);
}

private async updateVideoRecord(
videoRecordId: string,
videoUrl: string,
): Promise<void> {
const videoData = await this.videoService.update(videoRecordId, {
url: videoUrl,
});

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

private async removeGeneratedAvatars(
scenesWithGeneratedAvatars: SceneWithGeneratedAvatar[],
): Promise<unknown> {
return Promise.all(
scenesWithGeneratedAvatars.map((scene) => {
return this.azureAIService.removeAvatarVideo(scene.id);
}),
);
}

private async saveGeneratedAvatar(
scenesWithGeneratedAvatars: SceneWithGeneratedAvatar[],
): Promise<SceneWithGeneratedAvatar[]> {
const urls = await Promise.all(
scenesWithGeneratedAvatars.map(async (scene) => {
return this.saveAvatarVideo(scene.avatar.url, scene.id);
}),
);

return scenesWithGeneratedAvatars.map((scene, index) => ({
...scene,
avatar: {
url: urls[index] as string,
},
}));
}

private async removeAvatarsFromBucket(
scenesWithGeneratedAvatars: SceneWithGeneratedAvatar[],
): Promise<unknown> {
return Promise.all(
scenesWithGeneratedAvatars.map((scene) => {
return this.fileService.deleteFile(getFileName(scene.id));
}),
);
await this.scenesService.clearAvatars(compositionForRender.scenes);
}
}

Expand Down
6 changes: 4 additions & 2 deletions backend/src/bundles/avatar-videos/avatar-videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
import { videoService } from '../videos/videos.js';
import { AvatarVideoController } from './avatar-videos.controller.js';
import { AvatarVideoService } from './avatar-videos.service.js';
import { SceneService } from './scenes.service.js';

const scenesService = new SceneService({ azureAIService, fileService });

const avatarVideoService = new AvatarVideoService({
azureAIService,
fileService,
videoService,
remotionService,
scenesService,
});

const avatarVideoController = new AvatarVideoController(
Expand Down
Loading

0 comments on commit f69a23a

Please sign in to comment.