|
1 | | -import { Injectable, Logger } from '@nestjs/common'; |
| 1 | +import { Injectable, Logger,BadRequestException } from '@nestjs/common'; |
2 | 2 | import { HttpService } from '@nestjs/axios'; |
3 | 3 | import { firstValueFrom } from 'rxjs'; |
4 | 4 | import { NodeService } from '../node/node.service'; |
5 | 5 | import { ConfigService } from '@nestjs/config'; |
6 | | -import { RedisMessage } from '../subscriber/subscriber.service'; |
7 | 6 | import { PublisherService } from '@app/publisher'; |
| 7 | +import { AudioUploadDto } from './dto/audio.upload.dto'; |
| 8 | +import { AiDto } from './dto/ai.dto'; |
| 9 | +import OpenAI from 'openai'; |
| 10 | +import { OpenAiRequestDto } from './dto/openai.request.dto'; |
| 11 | +import { ClovaSpeechRequestDto } from './dto/clova.speech.request.dtd'; |
| 12 | +import { plainToInstance } from 'class-transformer'; |
| 13 | +import { OPENAI_PROMPT } from 'apps/api-server/src/common/constant'; |
| 14 | +import { RedisService } from '@liaoliaots/nestjs-redis'; |
| 15 | +import Redis from 'ioredis'; |
8 | 16 |
|
9 | 17 | export interface TextAiResponse { |
10 | 18 | keyword: string; |
11 | 19 | children: TextAiResponse[]; |
12 | 20 | } |
13 | | -const BAD_WORDS_REGEX = |
14 | | - /[시씨씪슈쓔쉬쉽쒸쓉](?:[0-9]*|[0-9]+*)[바발벌빠빡빨뻘파팔펄]|[섊좆좇졷좄좃좉졽썅춍봊]|[ᄌ조][0-9]*까|시바ᄅ?|ᄇ[0-9]*ᄉ|[ᄡᄲᇪᄺᄡᄣᄦᇠ]|[ᄉᄊᄴ][0-9]*[ᄁᄉᄊᄴᄇ]|[존좉좇][0-9]*나|[자보][0-9]+지|보빨|[봊봋봇봈볻봁봍]*[빨이]|[후훚훐훛훋훗훘훟훝훑][장앙]|[엠앰]창|애[미비]|애자|[가-탏탑-힣]색기|(?:[샊샛세쉐쉑쉨쉒객갞갟갯갰갴겍겎겏겤곅곆곇곗곘곜걕걖걗걧걨걬]*[끼키퀴])|새*[키퀴]|[병븅][0-9]*[신딱딲]|미친[가-닣닥-힣]|[믿밑]힌|[염옘][0-9]*병|[샊샛샜샠섹섺셋셌셐셱솃솄솈섁섂섓섔섘]기|[섹섺섻쎅쎆쎇쎽쎾쎿섁섂섃썍썎썏][스쓰]|[지야][0-9]*랄|니[애에]미|갈[0-9]*보[^가-힣]|[뻐뻑뻒뻙뻨][0-9]*[뀨큐킹낑)|꼬[0-9]*추|곧[0-9]*휴|[가-힣]슬아치|자[0-9]*박꼼|빨통|[사싸](?:이코|가지|[0-9]*까시)|육[0-9]*시[랄럴]|육[0-9]*실[알얼할헐]|즐[^가-힣]|찌[0-9]*(?:질이|랭이)|찐[0-9]*따|찐[0-9]*찌버거|창[녀놈]|[가-힣]{2,}충[^가-힣]|[가-힣]{2,}츙|부녀자|화냥년|환[양향]년|호[0-9]*[구모]|조[선센][징]|조센|[쪼쪽쪾](?:[발빨]이|[바빠]리)|盧|무현|찌끄[레래]기|(?:하악){2,}|하[앍앜]|[낭당랑앙항남담람암함][]?[가-힣]+[띠찌]|느[금급]마|文在|在寅|(?<=[^\n])[家哥]|속냐|[tT]l[qQ]kf|Wls|[ᄇ]신|[ᄉ]발|[ᄌ]밥/; |
15 | | -const CLOVA_X_PROMPT = |
16 | | - '- 당신은 텍스트 요약 어시스턴트입니다.\r\n- 주어진 텍스트를 분석하고 핵심 단어들을 추출해 대분류, 중분류, 소분류로 나눠주세요.\n- 반드시 대분류는 한개여야 합니다.\r\n- JSON 트리 구조의 데이터로 만들어주세요.\r\n- 각 객체에는 핵심 단어를 나타내는 keyword와 부모자식요소를 나타내는 children이 있으며, children의 경우 객체들을 포함한 배열입니다. \r\n- 마지막 자식 요소 또한 children을 필수적으로 빈 배열([])을 가지고 있습니다.\n- 개별 요소는 keyword (문자열), children (배열)을 가집니다.\n- keyword 는 최대한 짧고 간결하게 해주세요.\r\n- children 배열에는 개별 요소를 나타내는 객체가 들어갑니다.\r\n- children을 통해 나타내는 트리 구조의 깊이는 2를 넘을 수 없습니다.\r\n- keyword는 최대 50개로 제한을 둡니다.\n- 띄어쓰기와 줄바꿈 문자는 제거합니다.\n- 데이터 형태는 아래와 같습니다.\n- "{"keyword": "점심메뉴", "children": [{"keyword": "중식","children": [{"keyword": "짜장면","children": []},{"keyword": "짬뽕","children": []},{"keyword": "탕수육","children": []},{"keyword": "깐풍기","children": []}]},{"keyword": "일식","children": [{"keyword": "초밥","children": []},{"keyword": "오꼬노미야끼","children": []},{"keyword": "장어덮밥","children": []}]},{"keyword": "양식","children": [{"keyword": "파스타","children": []},{"keyword": "스테이크","children": []}]},{"keyword": "한식","children": [{"keyword": "김치찌개 ","children": []}]}]}" 와 같은 데이터 처럼 마지막 자식 요소가 자식이 없어도 빈 배열을 가지고 있어야합니다.'; |
17 | 21 |
|
18 | 22 | @Injectable() |
19 | 23 | export class AiService { |
20 | 24 | private readonly logger = new Logger(AiService.name); |
| 25 | + private readonly redis: Redis | null; |
21 | 26 | constructor( |
22 | 27 | private readonly configService: ConfigService, |
23 | 28 | private readonly httpService: HttpService, |
24 | 29 | private readonly nodeService: NodeService, |
25 | 30 | private readonly publisherService: PublisherService, |
26 | | - ) {} |
27 | | - |
28 | | - async requestClovaX(data: RedisMessage['data']) { |
29 | | - if (BAD_WORDS_REGEX.test(data.aiContent)) { |
30 | | - this.publisherService.publish( |
31 | | - 'api-socket', |
32 | | - JSON.stringify({ event: 'textAi', data: { error: '욕설이 포함되어 있습니다.' } }), |
33 | | - ); |
34 | | - return; |
35 | | - } |
36 | | - |
37 | | - const URL = this.configService.get('CLOVA_URL'); |
38 | | - const headers = { |
39 | | - 'X-NCP-CLOVASTUDIO-API-KEY': this.configService.get('X_NCP_CLOVASTUDIO_API_KEY'), |
40 | | - 'X-NCP-APIGW-API-KEY': this.configService.get('X_NCP_APIGW_API_KEY'), |
41 | | - 'X-NCP-CLOVASTUDIO-REQUEST-ID': this.configService.get('X_NCP_CLOVASTUDIO_REQUEST_ID'), |
42 | | - 'Content-Type': 'application/json', |
43 | | - }; |
44 | | - |
45 | | - const messages = [ |
46 | | - { |
47 | | - role: 'system', |
48 | | - content: CLOVA_X_PROMPT, |
49 | | - }, |
50 | | - { |
51 | | - role: 'user', |
52 | | - content: data.aiContent, |
53 | | - }, |
54 | | - ]; |
| 31 | + private readonly redisService: RedisService, |
| 32 | + ) { |
| 33 | + this.redis = redisService.getOrThrow('general'); |
| 34 | + } |
55 | 35 |
|
56 | | - const requestData = { |
57 | | - messages, |
58 | | - topP: 0.8, |
59 | | - topK: 0, |
60 | | - maxTokens: 2272, |
61 | | - temperature: 0.06, |
62 | | - repeatPenalty: 5.0, |
63 | | - stopBefore: [], |
64 | | - includeAiFilters: false, |
65 | | - seed: 0, |
66 | | - }; |
| 36 | + async requestOpenAi(aiDto: AiDto) { |
| 37 | + try { |
| 38 | + const aiCount = await this.redis.hget(aiDto.connectionId, 'aiCount'); |
| 39 | + if (Number(aiCount) <= 0) { |
| 40 | + this.publisherService.publish('api-socket', { |
| 41 | + event: 'textAiSocket', |
| 42 | + data: { error: 'AI 사용 횟수가 모두 소진되었습니다.', connectionId: aiDto.connectionId }, |
| 43 | + }); |
| 44 | + return; |
| 45 | + } |
| 46 | + const apiKey = this.configService.get('OPENAI_API_KEY'); |
| 47 | + const openai = new OpenAI(apiKey); |
67 | 48 |
|
68 | | - const response = await firstValueFrom(this.httpService.post(URL, requestData, { headers })); |
| 49 | + const openAiRequestDto = new OpenAiRequestDto(); |
| 50 | + openAiRequestDto.setPrompt(OPENAI_PROMPT); |
| 51 | + openAiRequestDto.setAiContent(aiDto.aiContent); |
69 | 52 |
|
70 | | - letresult: string=response.data.result.message.content; |
| 53 | + constresponse=awaitopenai.chat.completions.create(openAiRequestDto.toObject()); |
71 | 54 |
|
72 | | - if (result[result.length - 1] !== '}') { |
73 | | - result = result + '}'; |
| 55 | + const result = JSON.parse(response.choices[0].message.content) as TextAiResponse; |
| 56 | + console.log(result); |
| 57 | + const nodeData = await this.nodeService.aiCreateNode(result, aiDto.mindmapId); |
| 58 | + this.publisherService.publish('api-socket', { |
| 59 | + event: 'textAiSocket', |
| 60 | + data: { nodeData, connectionId: aiDto.connectionId }, |
| 61 | + }); |
| 62 | + } catch (error) { |
| 63 | + this.logger.error('OPENAI 요청 에러 : ' + error); |
| 64 | + this.publisherService.publish('api-socket', { |
| 65 | + event: 'textAiSocket', |
| 66 | + data: { error: '텍스트 변환 요청에 실패했습니다.', connectionId: aiDto.connectionId }, |
| 67 | + }); |
74 | 68 | } |
| 69 | + } |
75 | 70 |
|
76 | | - const resultJson = JSON.parse(result) as TextAiResponse; |
77 | | - const nodeData = await this.nodeService.aiCreateNode(resultJson, Number(data.mindmapId)); |
| 71 | + async requestClovaSpeech(audioFile: Express.Multer.File, audioUploadDto: AudioUploadDto) { |
| 72 | + try { |
| 73 | + const URL = this.configService.get('CLOVA_SPEECH_URL'); |
| 74 | + const apiKey = this.configService.get('X_CLOVASPEECH_API_KEY'); |
| 75 | + const formData = new ClovaSpeechRequestDto(apiKey, audioFile); |
| 76 | + const response = await firstValueFrom( |
| 77 | + this.httpService.post(URL, formData.getFormData(), { headers: formData.getHeaders() }), |
| 78 | + ); |
| 79 | + const result = response.data.text; |
78 | 80 |
|
79 | | - this.publisherService.publish( |
80 | | - 'api-socket', |
81 | | - JSON.stringify({ event: 'textAiSocket', data: { nodeData, connectionId: data.connectionId } }), |
82 | | - ); |
| 81 | + const aiDto = plainToInstance(AiDto, { |
| 82 | + aiContent: result, |
| 83 | + connectionId: audioUploadDto.connectionId, |
| 84 | + mindmapId: audioUploadDto.mindmapId, |
| 85 | + }); |
| 86 | + await this.requestOpenAi(aiDto); |
| 87 | + } catch (error) { |
| 88 | + this.logger.error('CLOVA-SPEECH 요청 에러 : ' + error); |
| 89 | + this.publisherService.publish('api-socket', { |
| 90 | + event: 'textAiSocket', |
| 91 | + data: { error: '음성 변환 요청에 실패했습니다.', connectionId: audioUploadDto.connectionId }, |
| 92 | + }); |
| 93 | + throw new BadRequestException('음성 변환 요청에 실패했습니다.'); |
| 94 | + } |
83 | 95 | } |
84 | 96 | } |
0 commit comments