Skip to content

Commit

Permalink
feat: ✨ (api) transcribeYoutubeVideo を実装
Browse files Browse the repository at this point in the history
  • Loading branch information
dino3616 committed Mar 16, 2024
1 parent ebf7208 commit e0a89de
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 5 deletions.
4 changes: 4 additions & 0 deletions apps/api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ schema.gql
# auto generated folder
generated

# tmp files
tmp/*
!tmp/.gitkeep

# certificates
*.crt
4 changes: 4 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"dependencies": {
"@as-integrations/fastify": "2.1.1",
"@distube/ytdl-core": "4.13.3",
"@fastify/compress": "7.0.0",
"@fastify/cors": "9.0.1",
"@fastify/helmet": "11.1.1",
Expand All @@ -33,7 +34,9 @@
"@nestjs/core": "10.3.3",
"@nestjs/platform-fastify": "10.3.3",
"@nestjs/testing": "10.3.3",
"fluent-ffmpeg": "2.1.2",
"langchain": "0.1.27",
"openai": "4.29.1",
"ts-pattern": "5.0.8",
"uuid": "9.0.1",
"zod": "3.22.4"
Expand All @@ -42,6 +45,7 @@
"@hanjaemeo-api/tsconfig": "workspace:*",
"@hanjaemeo-api/type": "workspace:*",
"@nestjs/schematics": "10.1.1",
"@types/fluent-ffmpeg": "2.1.24",
"@types/uuid": "9.0.8"
}
}
1 change: 0 additions & 1 deletion apps/api/src/common/service/env/env.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export class EnvService {
private readonly logger = new Logger(EnvService.name);

constructor(@Inject(ConfigService) private readonly configService: ConfigService) {
this.logger.debug(`${EnvService.name} constructed`);
this.logger.log(`NODE_ENV: ${this.NodeEnv}`);
}

Expand Down
27 changes: 27 additions & 0 deletions apps/api/src/infra/langchain/langchain.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Controller, Get, Inject, Logger, Param } from '@nestjs/common';
import { OpenaiService } from '#api/infra/openai/openai.service';
import { YoutubeService } from '../youtube/youtube.service';

@Controller()
export class LangchainController {
private readonly logger = new Logger(LangchainController.name);

constructor(
@Inject(OpenaiService)
private readonly openaiService: OpenaiService,
@Inject(YoutubeService)
private readonly youtubeService: YoutubeService,
) {}

@Get('/predict/:id')
async transcribeYoutubeVideo(@Param('id') id: string): Promise<string> {
this.logger.log(`${this.transcribeYoutubeVideo.name} called`);
this.logger.debug(`Transcribing video: https://www.youtube.com/watch?v=${id}`);

const outputFilePath = await this.youtubeService.saveVideoStreamToFile(`https://www.youtube.com/watch?v=${id}`);

const transcription = await this.openaiService.transcribeFromFile(outputFilePath);

return transcription;
}
}
5 changes: 5 additions & 0 deletions apps/api/src/infra/langchain/langchain.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Global, Module } from '@nestjs/common';
import { OpenaiModule } from '#api/infra/openai/openai.module';
import { YoutubeModule } from '#api/infra/youtube/youtube.module';
import { LangchainController } from './langchain.controller';
import { LangchainService } from './langchain.service';

@Global()
@Module({
imports: [OpenaiModule, YoutubeModule],
controllers: [LangchainController],
providers: [LangchainService],
exports: [LangchainService],
})
Expand Down
4 changes: 1 addition & 3 deletions apps/api/src/infra/langchain/langchain.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ export class LangchainService {

private readonly logger = new Logger(LangchainService.name);

constructor(@Inject(EnvService) private readonly envService: EnvService) {
this.logger.debug(`${LangchainService.name} constructed`);
}
constructor(@Inject(EnvService) private readonly envService: EnvService) {}

async embedding(text: string) {
if (!this.embeddingModel) {
Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/infra/openai/openai.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { OpenaiService } from './openai.service';

@Module({
providers: [OpenaiService],
exports: [OpenaiService],
})
export class OpenaiModule {}
25 changes: 25 additions & 0 deletions apps/api/src/infra/openai/openai.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
import { OpenAI } from 'openai';
import { EnvService } from '#api/common/service/env/env.service';

@Injectable()
export class OpenaiService {
private readonly openai: OpenAI;

constructor(@Inject(EnvService) private readonly envService: EnvService) {
this.openai = new OpenAI({ apiKey: this.envService.OpenaiApiKey });
}

async transcribeFromFile(path: string): Promise<string> {
const res = await this.openai.audio.transcriptions.create({
model: 'whisper-1',
language: 'ko',
timestamp_granularities: ['segment'],
response_format: 'verbose_json',
file: fs.createReadStream(path),
});

return res.text;
}
}
8 changes: 8 additions & 0 deletions apps/api/src/infra/youtube/youtube.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { YoutubeService } from './youtube.service';

@Module({
providers: [YoutubeService],
exports: [YoutubeService],
})
export class YoutubeModule {}
74 changes: 74 additions & 0 deletions apps/api/src/infra/youtube/youtube.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import fs from 'node:fs';
import ytdl from '@distube/ytdl-core';
import { Injectable, Logger } from '@nestjs/common';
import ffmpeg from 'fluent-ffmpeg';

@Injectable()
export class YoutubeService {
private readonly logger = new Logger(YoutubeService.name);

async saveVideoStreamToFile(url: string): Promise<string> {
const videoInfo = await this.getVideoInfo(url);

const downloadedPath = this.isAlreadyDownloaded(videoInfo.videoDetails.videoId)
? `./tmp/${videoInfo.videoDetails.videoId}.mp4`
: await this.getSoundOnlyVideoStream(videoInfo);

const audioFilePath = this.isAlreadyConverted(videoInfo.videoDetails.videoId)
? `./tmp/${videoInfo.videoDetails.videoId}.mp3`
: await this.convertToMp3(downloadedPath, videoInfo.videoDetails.videoId);

return audioFilePath;
}

private async getVideoInfo(url: string) {
const videoInfo = await ytdl.getInfo(url);

return videoInfo;
}

private isAlreadyDownloaded(id: string): boolean {
const downloaded = fs.existsSync(`./tmp/${id}.mp4`);

return downloaded;
}

private async getSoundOnlyVideoStream(videoInfo: ytdl.videoInfo): Promise<string> {
const stream = await ytdl
.downloadFromInfo(videoInfo, {
filter: 'audioonly',
requestOptions: {
reset: true,
},
})
.pipe(fs.createWriteStream(`./tmp/${videoInfo.videoDetails.videoId}.mp4`));

this.logger.debug(`Downloaded: ${videoInfo.videoDetails.videoId}`);

return stream.path as string;
}

private isAlreadyConverted(id: string): boolean {
const downloaded = fs.existsSync(`./tmp/${id}.mp3`);

return downloaded;
}

private async convertToMp3(inputPath: string, id: string): Promise<string> {
await new Promise<void>((resolve, reject) => {
ffmpeg(inputPath)
.outputFormat('mp3')
.on('end', () => {
resolve(void undefined);
})
.on('error', error => {
reject(error);
})
.save(`./tmp/${id}.mp3`);
});

this.logger.debug(`Converted to mp3: ${id}`);

return `./tmp/${id}.mp3`;
}
}
Empty file added apps/api/tmp/.gitkeep
Empty file.
Binary file modified bun.lockb
Binary file not shown.
8 changes: 7 additions & 1 deletion docker/api/Dockerfile.production
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ RUN bun install --frozen-lockfile --ignore-scripts
# hadolint ignore=DL3059
RUN bun turbo --filter='@hanjaemeo-api/api' build

FROM oven/bun:1.0.30-alpine AS runner
FROM oven/bun:1.0.30-slim AS runner
ENV NODE_ENV=production

# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get --no-install-recommends -y install ffmpeg \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists

WORKDIR /hanjaemeo-api/

COPY --from=builder /hanjaemeo-api/node_modules/ ./node_modules/
Expand Down

0 comments on commit e0a89de

Please sign in to comment.