Skip to content

Commit

Permalink
Merge pull request #4 from goormthon-Univ/develope
Browse files Browse the repository at this point in the history
Develope
  • Loading branch information
Sirius506775 authored Mar 17, 2024
2 parents 9e55bd9 + 59ae59c commit 3824e84
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 3 deletions.
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/✅-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: "✅ FEATURE"
about: Feature 작업 사항을 입력해주세요.
title: ''
labels: ''
assignees: ''

---

### Description
> 설명을 작성해주세요.
### In Progress
> 작업사항들을 작성해주세요.
- [ ] todo1
- [ ] todo2
- [ ] todo3

### ETC
> 기타사항
27 changes: 27 additions & 0 deletions .github/ISSUE_TEMPLATE/🐛-bug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: "\U0001F41B BUG"
about: bug 발생 시 작성해주세요.
title: ''
labels: ''
assignees: ''

---

### Describe the bug
> 버그에 대해 설명해주세요.
### To Reproduce
> 버그 발생 과정을 기술하세요.
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

### Expected behavior
> 본래 작동할 것이라 예상했던 구현 결과에 대해 설명하세요.
### Screenshots
> 화면 첨부파일
### Additional context(Optional)
>발생한 문제에 대한 추가사항
10 changes: 10 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## ☑️ Describe your changes
- 작업 내용을 적어주세요
> ex. - CORS 허용 범위를 (/**) URL 전체로 변경한다.
## 📷 Screenshot
- 관련 스크린샷

## 🔗 Issue number and link
- 이슈 번호를 등록해주세요
> closed {#이슈번호}
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1'

// https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-polly
implementation 'com.amazonaws:aws-java-sdk-polly:1.12.681'


}

Expand Down
97 changes: 97 additions & 0 deletions src/main/java/site/balpyo/ai/controller/PollyController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package site.balpyo.ai.controller;

import com.amazonaws.util.IOUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import site.balpyo.ai.dto.PollyDTO;
import site.balpyo.ai.service.PollyService;
import site.balpyo.common.dto.CommonResponse;
import site.balpyo.common.dto.ErrorEnum;

import java.io.IOException;
import java.io.InputStream;


/**
* @author dongheonlee
* AWS polly를 활용한 tts 구현 컨트롤러
*/
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/polly")
public class PollyController {

@Value("${secrets.BALPYO_API_KEY}") //TODO :: 임시 api 시크릿 키 구현 (차후 로그인 연동시 삭제예정)
public String BALPYO_API_KEY;

private final PollyService pollyService;

/**
* @param pollyDTO
* @return 호출 시, 요청정보에 따른 mp3 음성파일을 반환(audioBytes)한다.
*/
@PostMapping("/generateAudio")
public ResponseEntity<?> synthesizeText(@RequestBody PollyDTO pollyDTO) {

if (!BALPYO_API_KEY.equals(pollyDTO.getBalpyoAPIKey())) {
return CommonResponse.error(ErrorEnum.BALPYO_API_KEY_ERROR);
}

try {
// Amazon Polly와 통합하여 텍스트를 음성으로 변환
InputStream audioStream = pollyService.synthesizeSpeech(pollyDTO);

// InputStream을 byte 배열로 변환
byte[] audioBytes = IOUtils.toByteArray(audioStream);

// MP3 파일을 클라이언트에게 반환
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("testAudio", "speech.mp3");

return ResponseEntity.ok()
.headers(headers)
.body(audioBytes);

} catch (IOException e) {
e.printStackTrace();
return CommonResponse.error(ErrorEnum.INTERNAL_SERVER_ERROR);
}
}


/**
* 입력한 대본을 음성 파일 변환 시, 음절당 기준값에 따라 계산된 소요시간을 반환한다.
* @param pollyDTO
* @return
*/
@PostMapping("/estimateDuration")
public ResponseEntity<CommonResponse> estimateSpeechDuration(@RequestBody PollyDTO pollyDTO) {

if (!BALPYO_API_KEY.equals(pollyDTO.getBalpyoAPIKey())) {
return CommonResponse.error(ErrorEnum.BALPYO_API_KEY_ERROR);
}

String inputText = pollyDTO.getText();

try {
int durationInSeconds = pollyService.estimateSpeechDuration(inputText);


return CommonResponse.success(durationInSeconds);

} catch (Exception e) {
e.printStackTrace();
return CommonResponse.error(ErrorEnum.INTERNAL_SERVER_ERROR);
}
}
}
17 changes: 17 additions & 0 deletions src/main/java/site/balpyo/ai/dto/EstimateRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package site.balpyo.ai.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
* @author dongheonlee
*/
@Data
public class EstimateRequestDTO {

// 소요시간을 계산할 입력 텍스트
@NotBlank(message = "text는 비어 있을 수 없습니다.")
private String text;

}
23 changes: 23 additions & 0 deletions src/main/java/site/balpyo/ai/dto/PollyDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package site.balpyo.ai.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
* @author dongheonlee
*/
@Data
public class PollyDTO {

// 음성으로 변환할 텍스트
@NotBlank(message = "text는 비어 있을 수 없습니다.")
private String text;

// 빠르기 조절 [-2, -1, 0, 1, 2]
@NotBlank(message = "speed는 비어 있을 수 없습니다.")
private int speed;

@NotBlank(message = "balpyoSecretKey는 비어 있을 수 없습니다.")
private String balpyoAPIKey;
}
11 changes: 10 additions & 1 deletion src/main/java/site/balpyo/ai/service/AIGenerateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public String createPromptString(String topic, String keywords , Integer sec){
"\n" +
"I want you to act as a presenter specialized in "+ topic +" My first request is for you to generate a script:\n" +
"\n" +
"Make a script by calculating 150ms per syllable except spaces and commas" +
"Here's some context:\n" +
"Topic - " + topic +"\n" +
"Keywords - " + keywords +"\n" +
Expand Down Expand Up @@ -51,6 +52,14 @@ public ResponseEntity<Map> requestGPTTextGeneration(String prompt, float tempera
}



// public ResponseEntity<>Map> requestGPTTextToSpeech(){
//
// HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
//
// RestTemplate restTemplate = new RestTemplate();
// ResponseEntity<Map> response = restTemplate.postForEntity(ENDPOINT, requestEntity, Map.class);
//
// return new response;
// }

}
104 changes: 104 additions & 0 deletions src/main/java/site/balpyo/ai/service/PollyService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package site.balpyo.ai.service;

import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.polly.AmazonPolly;
import com.amazonaws.services.polly.AmazonPollyClient;
import com.amazonaws.services.polly.model.*;
import org.springframework.stereotype.Service;
import site.balpyo.ai.dto.PollyDTO;

import java.io.InputStream;

/**
* @author dongheonlee
*/
@Service
public class PollyService {

/**
* 입력된 텍스트의 예상 음성 파일 재생 시간을 계산하여 반환한다.
*
* @param text 입력 텍스트
* @return 예상 음성 파일 재생 시간 (초)
*/
public int estimateSpeechDuration(String text) {
// 예상 소요 시간 계산을 위한 변수
int totalDuration = 0;

// 입력 텍스트에서 "\n"과 "."을 제외한 문자열 추출
String cleanedText = text.replaceAll("[\\n.]", "");

// 입력 텍스트를 한 음절씩 분리하여 처리
for (char c : cleanedText.toCharArray()) {
// 띄어쓰기나 쉼표가 있을 때마다 한 번씩 숨을 쉬는 시간 추가
if (c == ' ' || c == ',') {
totalDuration += 1000; // 1초의 숨쉬는 시간으로 가정
} else {
int durationPerSyllable = 150; // 음절당 150ms로 설정
totalDuration += durationPerSyllable;
}
}

// 소요 시간을 초로 변환하여 반환
return totalDuration / 1000; // ms를 초로 변환
}


/**
* 입력된 텍스트와 선택된 빠르기에 따라 음성파일으로 변환하여 반환한다.
*
* @param pollyDTO
* @return mp3 오디오 파일
*/
public InputStream synthesizeSpeech(PollyDTO pollyDTO) {

String inputText = pollyDTO.getText();
int speed = pollyDTO.getSpeed();

// Amazon Polly 클라이언트 생성
AmazonPolly amazonPolly = AmazonPollyClient.builder()
.withRegion(Regions.AP_NORTHEAST_2) // 서울 리전
.withCredentials(new DefaultAWSCredentialsProviderChain())
.build();

// 빠르기 계산
float relativeSpeed = calculateRelativeSpeed(speed);

// SynthesizeSpeechRequest 생성
SynthesizeSpeechRequest synthesizeSpeechRequest = new SynthesizeSpeechRequest()
.withText(inputText)
.withOutputFormat(OutputFormat.Mp3) // MP3 형식
.withVoiceId(VoiceId.Seoyeon) // 한국어 음성 변환 보이스
.withTextType("ssml") // SSML 형식 사용 -> <prosody> 태그와 rate로 설정 가능
.withText("<speak><prosody rate=\"" + relativeSpeed + "\">" + inputText + "</prosody></speak>");

// 텍스트를 음성으로 변환하여 InputStream으로 반환
SynthesizeSpeechResult synthesizeSpeechResult = amazonPolly.synthesizeSpeech(synthesizeSpeechRequest);
return synthesizeSpeechResult.getAudioStream();
}


/**
* mp3 audio 생성 시, 빠르기 설정 메소드
*/
private static float calculateRelativeSpeed(int speed) {
// 기본 속도
float baseSpeed = 1.0f;

switch (speed) {
case -2:
return baseSpeed * 0.5f;
case -1:
return baseSpeed * 0.8f;
case 1:
return baseSpeed * 1.2f;
case 2:
return baseSpeed * 1.5f;
default:
return baseSpeed;
}
}


}
7 changes: 5 additions & 2 deletions src/main/java/site/balpyo/common/dto/ErrorEnum.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package site.balpyo.common.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

@Getter
Expand All @@ -13,7 +12,11 @@ public enum ErrorEnum {
GPT_API_KEY_MISSING("8002", "GPT API 키 누락."),

//9000 - client 계열 에러
BALPYO_API_KEY_ERROR("9000", "BALPYO_API_KEY를 다시 확인해주세요.");
BALPYO_API_KEY_ERROR("9000", "BALPYO_API_KEY를 다시 확인해주세요."),

// 5000 - 내부 서버 에러
INTERNAL_SERVER_ERROR("5000", "내부 서버 오류가 발생했습니다.");


private final String code;
private final String message;
Expand Down

0 comments on commit 3824e84

Please sign in to comment.