Skip to content

Commit

Permalink
Merge pull request #302 from boostcampwm-2024/feat/#92/api-summary
Browse files Browse the repository at this point in the history
[Feat] 오디오 텍스트로 변환 및 요약 로직 구현
  • Loading branch information
Jieun1ee authored Dec 2, 2024
2 parents a6617fd + 1235417 commit 32b0201
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 8 deletions.
9 changes: 3 additions & 6 deletions apps/api/src/entity/summary.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ export class Summary {
id: number;

@Column('varchar')
name: string;
audioUrl: string;

@Column('varchar')
source: string;

@Column('varchar')
summary: string;
@Column('json')
summaryText: string[];

@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
createdAt: Date;
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/stream/dto/createSummaryDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class CreateSummaryDto {
ticleId: number;
audioUrl: string;
}
25 changes: 23 additions & 2 deletions apps/api/src/stream/stream.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import { Controller } from '@nestjs/common';
import { Body, Controller, Get, Param, Post } from '@nestjs/common';

import { CreateSummaryDto } from './dto/createSummaryDto';
import { StreamService } from './stream.service';

@Controller('stream')
export class StreamController {}
export class StreamController {
constructor(private readonly streamService: StreamService) {}

@Post('audio')
async getAudioFileUrl(@Body() createSummaryDto: CreateSummaryDto) {
const summary = await this.streamService.createSummary(createSummaryDto);

const fulltext = await this.streamService.transcribeAudio(createSummaryDto.audioUrl);
const summaryResult = await this.streamService.summaryAudio(fulltext);

await this.streamService.updateSummaryText(summary, summaryResult);
}

@Get('summary/:ticleId')
async getSummaryByTicleId(@Param('ticleId') ticleId: number) {
const text = await this.streamService.getSummaryText(ticleId);
return { summary: text };
}
}
7 changes: 7 additions & 0 deletions apps/api/src/stream/stream.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { Summary } from '@/entity/summary.entity';
import { Ticle } from '@/entity/ticle.entity';

import { StreamController } from './stream.controller';
import { StreamService } from './stream.service';

@Module({
imports: [TypeOrmModule.forFeature([Ticle, Summary])],
controllers: [StreamController],
providers: [StreamService],
})
export class StreamModule {}
117 changes: 117 additions & 0 deletions apps/api/src/stream/stream.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ErrorMessage } from '@repo/types';

import { Summary } from '@/entity/summary.entity';
import { Ticle } from '@/entity/ticle.entity';

import { CreateSummaryDto } from './dto/createSummaryDto';

@Injectable()
export class StreamService {
constructor(
@InjectRepository(Summary)
private summaryRepository: Repository<Summary>,
@InjectRepository(Ticle)
private ticleRepository: Repository<Ticle>,
private configService: ConfigService
) {}

async createSummary(createSummaryDto: CreateSummaryDto) {
const { ticleId, audioUrl } = createSummaryDto;
const ticle = await this.ticleRepository.findOne({
where: { id: ticleId },
});

if (!ticle) {
throw new NotFoundException(ErrorMessage.TICLE_NOT_FOUND);
}

const summary = this.summaryRepository.create({
audioUrl: audioUrl,
ticle: ticle,
});
return await this.summaryRepository.save(summary);
}

async transcribeAudio(remoteFileName: string) {
const speechEndpoint = this.configService.get<string>('CLOVASPEECH_ENDPOINT');

const body = {
dataKey: remoteFileName,
params: { enable: true },
language: 'ko-KR',
completion: 'sync',
fulltext: true,
};

try {
const response = await fetch(speechEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CLOVASPEECH-API-KEY': this.configService.get<string>('CLOVASPEECH_API_KEY'),
},
body: JSON.stringify(body),
});

const data = await response.json();
return data.text;
} catch (error) {
console.error('Failed to transcribe audio:', error);
throw new InternalServerErrorException(ErrorMessage.FAILED_TO_TRANSCRIBE_AUDIO);
}
}

async summaryAudio(text: string) {
const studioEndpoint = this.configService.get<string>('CLOVASTUDIO_ENDPOINT');

const body = {
texts: [text],
segMinSize: 300,
includeAiFilters: true,
autoSentenceSplitter: true,
segCount: -1,
segMaxSize: 1000,
};

try {
const response = await fetch(studioEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-NCP-CLOVASTUDIO-API-KEY': this.configService.get<string>('CLOVASTUDIO_API_KEY'),
'X-NCP-APIGW-API-KEY': this.configService.get<string>('CLOVASTUDIO_APIGW_API_KEY'),
},
body: JSON.stringify(body),
});

if (!response.ok) {
const errorText = await response.text();
console.error('Error response:', errorText);
throw new Error(`API error: ${response.status} - ${response.statusText}`);
}

const data = await response.json();

return data.result.text;
} catch (error) {
console.error('Failed to summarize audio:', error);
throw new InternalServerErrorException(ErrorMessage.FAILED_TO_SUMMARY_AUDIO);
}
}

async getSummaryText(ticleId: number) {
const summary = await this.summaryRepository.findOne({
where: { ticle: { id: ticleId } },
});
return summary.summaryText;
}

async updateSummaryText(summary: Summary, summaryText: string[]) {
summary.summaryText = summaryText;
await this.summaryRepository.save(summary);
}
}
2 changes: 2 additions & 0 deletions packages/types/src/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const ErrorMessage = {
FAILED_TO_CREATE_TICLE: '티클 생성에 실패했습니다',
CANNOT_REQUEST_OWN_TICLE: '자신이 발표자인 티클에는 신청할 수 없습니다',
TICLE_ALREADY_REQUESTED: '이미 신청한 티클입니다',
FAILED_TO_TRANSCRIBE_AUDIO: 'CLOVA SPEECH로 텍스트 변환에 실패했습니다',
FAILED_TO_SUMMARY_AUDIO: 'CLOVA STRUDIO로 요약에 실패했습니다',
CANNOT_DELETE_OTHERS_TICLE: '다른 사람의 티클은 삭제할 수 없습니다',
CANNOT_START_TICLE: '티클을 시작할 수 없습니다',
CANNOT_END_TICLE: '티클을 종료할 수 없습니다',
Expand Down

0 comments on commit 32b0201

Please sign in to comment.