Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat : Converting to voice files based on input text and selected speed #4

Merged
merged 7 commits into from
Mar 17, 2024
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
Loading