Skip to content

Commit

Permalink
feat: 캐스트 API 완성 (#46)
Browse files Browse the repository at this point in the history
* fix: Cast 엔티티 수정
Voice와 Formality 분리

* add: tts 기능 구현 용 파일 추가

* fix: 파일 구조 오류 수정

* fix: tts 로직 임시 구현(파싱 로직 구현 후 refactoring 예정)

* add: dto로 요청 값 받기

* feat: 스크립트 생성 기능 (#21)

* fix: voice 칼럼 String으로 변경

* add: OpenAI 라이브러리
https://github.com/TheoKanning/openai-java

* add: validation 라이브러리 추가
@Valid가 작동 안하길래 찾아봤는데 springboot-starter-validation 라이브러리를 따로 추가해야된다고 함

* feat: ScriptService 추가
- 캐스트 생성 요청은 CastCreationRequestDTO로 받음
- 프롬프트 생성 (ChatGPTPromptGenerator)
- 스크립트 생성 (ChatGPTScriptGenerator)
- ScriptService로 묶음

* add: CastController, CastService 추가
- 회의때 얘기한 클래스 구조로 ㄱㄱ

* fix: 수정사항 반영
- CastController에 raiseError 함수 삭제
- OpenAiService 응답 대기 시간 30초로 늘림

* add: @구분자를 통한 parsing 로직 구현

* add: script parsing 후 time point marking 후 ssml요청 로직 구현

* add: ssml 결과물(mp3) 파일 프로젝트 내부(임시)에 저장 로직 구현

* fix: 코드 정리

* add: sentence에 timepoint column 추가

* update: mp3 파일 무시

* add: script 번역 기능 구현 (#28)

* feat: S3 관련 파일 업로드 설정 (#34)

* fix: ci/cd 구축 변경 (#31)

* add: S3 설정 추가 및 mp3 upload 로직 반영

* fix: 불필요한 코드 정리

* feat: 캐스트 재생 (스트리밍) (#33)

* feat: streaming 기능 추가

* add: 테스트용 파일 추가
- 나중에 삭제 해주세용

* add: 스트리밍 API 추가
- 일단 ~/stream-test랑 ~/stream/{filename}으로 정함

* fix: filePath 문자열로 타입 변경

* add: castId 스트리밍 추가

* add : sentence entity 저장 로직

* add : voice code enum 처리

* [FEATURE] Voice Enum 처리 & Sentence save 로직 구현 (#36)

* add : sentence entity 저장 로직

* add : voice code enum 처리

---------

Co-authored-by: yuuddin <[email protected]>

* add: 필요한 DTO/서비스/리포지토리 선언
- CastPlaylistRepository/Service
- CastSaveDTO / CastUpdateDTO
- PlaylistRepository
- ScriptCastCreationDTO
- KeywordCastCreationDTO

* add: Cast 필요 메소드
- update(): 정보 수정
- updateHits(): 조회수 증가

* feat: cast api 구현

* fix: Cast에 Sentence 매핑 추가 + 연관관계 편의 메소드

* fix: 컬렉션 초기화 안되는 문제
Lombok에서 @builder 쓸 때 컬렉션 초기화하려면
- final이거나
- @Builder.Default 붙이거나 해야되는데
일단 후자로 했습니다

* update: API가 엔티티 대신 dto 반환하도록 변경

* add: DTO 클래스 추가

* feat: 캐스트 스크립트 검색 API 추가
- GET /cast/{id}/script로 해당 캐스트 스크립트 가져옴
- 캐스트 재생은 GET /cast/{id}에서 GET /cast/{id}/audio로 변경 (일관성)
- 이에 따른 SentenceService, SentenceRepository 등 변경

* feat: Cast API 구현 (#37)

* add: 필요한 DTO/서비스/리포지토리 선언
- CastPlaylistRepository/Service
- CastSaveDTO / CastUpdateDTO
- PlaylistRepository
- ScriptCastCreationDTO
- KeywordCastCreationDTO

* add: Cast 필요 메소드
- update(): 정보 수정
- updateHits(): 조회수 증가

* feat: cast api 구현

* fix: Cast에 Sentence 매핑 추가 + 연관관계 편의 메소드

* fix: 컬렉션 초기화 안되는 문제
Lombok에서 @builder 쓸 때 컬렉션 초기화하려면
- final이거나
- @Builder.Default 붙이거나 해야되는데
일단 후자로 했습니다

* update: API가 엔티티 대신 dto 반환하도록 변경

* add: DTO 클래스 추가

* feat: 캐스트 스크립트 검색 API 추가
- GET /cast/{id}/script로 해당 캐스트 스크립트 가져옴
- 캐스트 재생은 GET /cast/{id}에서 GET /cast/{id}/audio로 변경 (일관성)
- 이에 따른 SentenceService, SentenceRepository 등 변경

* fix : cast 생성 로직 수정

* add : cast audi length 입력

* fix: multipartfile 입력 형태 수정

* fix: 코드 정리

* fix: CastScriptDTO에 스크립트가 비는 문제 해결
- sentenceService.save()가 List<Sentence>를 반환하도록 변경

* add: StringUtil 추가
- String 비었는지 확인하는 기능

* add: deleteCast 추가, setCastImage 추가

* fix: update 조건 수정
- blank가 아닐 떄 수정해야하는데 그 반대로 하고 있었네..

* fix: CastPlaylist 동일한 행 존재하면 기각

* fix: Long playlistId 검사조건 @NotNull로 변경

* update: 수정, 저장, 삭제 API 추가 + @RequestPart 오류 수정 + stream에 @crossorigin 추가

* fix: stream 기능 업데이트
- aws에 파일 업로드함에 따른 변경

* feat: Cast API 완성 (#42)

* add : sentence entity 저장 로직

* add : voice code enum 처리

* add: 필요한 DTO/서비스/리포지토리 선언
- CastPlaylistRepository/Service
- CastSaveDTO / CastUpdateDTO
- PlaylistRepository
- ScriptCastCreationDTO
- KeywordCastCreationDTO

* add: Cast 필요 메소드
- update(): 정보 수정
- updateHits(): 조회수 증가

* feat: cast api 구현

* fix: Cast에 Sentence 매핑 추가 + 연관관계 편의 메소드

* fix: 컬렉션 초기화 안되는 문제
Lombok에서 @builder 쓸 때 컬렉션 초기화하려면
- final이거나
- @Builder.Default 붙이거나 해야되는데
일단 후자로 했습니다

* update: API가 엔티티 대신 dto 반환하도록 변경

* add: DTO 클래스 추가

* feat: 캐스트 스크립트 검색 API 추가
- GET /cast/{id}/script로 해당 캐스트 스크립트 가져옴
- 캐스트 재생은 GET /cast/{id}에서 GET /cast/{id}/audio로 변경 (일관성)
- 이에 따른 SentenceService, SentenceRepository 등 변경

* fix : cast 생성 로직 수정

* add : cast audi length 입력

* fix: multipartfile 입력 형태 수정

* fix: 코드 정리

* fix: CastScriptDTO에 스크립트가 비는 문제 해결
- sentenceService.save()가 List<Sentence>를 반환하도록 변경

* add: StringUtil 추가
- String 비었는지 확인하는 기능

* add: deleteCast 추가, setCastImage 추가

* fix: update 조건 수정
- blank가 아닐 떄 수정해야하는데 그 반대로 하고 있었네..

* fix: CastPlaylist 동일한 행 존재하면 기각

* fix: Long playlistId 검사조건 @NotNull로 변경

* update: 수정, 저장, 삭제 API 추가 + @RequestPart 오류 수정 + stream에 @crossorigin 추가

* fix: stream 기능 업데이트
- aws에 파일 업로드함에 따른 변경

---------

Co-authored-by: yuuddin <[email protected]>

* fix: 머지 중 오류 변경

---------

Co-authored-by: yuuddin <[email protected]>
Co-authored-by: yuuddin <[email protected]>
Co-authored-by: yuuddin <[email protected]>
  • Loading branch information
4 people authored Aug 11, 2024
1 parent 822a967 commit f3d3d26
Show file tree
Hide file tree
Showing 42 changed files with 1,342 additions and 38 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ out/
.vscode/

### 추가로 무시할 파일 ###
*.yml
*.yml
*.mp3
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dependencies {
// OpenAI API 라이브러리
implementation 'com.theokanning.openai-gpt3-java:service:0.18.2'
implementation 'com.google.code.gson:gson:2.9.0'

//jwt
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
Expand Down
1 change: 0 additions & 1 deletion src/main/java/com/umc/owncast/OwncastApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

@EnableJpaAuditing
@SpringBootApplication
@EnableJpaAuditing
public class OwncastApplication {

public static void main(String[] args) {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/umc/owncast/common/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.umc.owncast.common.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/umc/owncast/common/config/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.umc.owncast.common.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AwsS3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
AWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
17 changes: 16 additions & 1 deletion src/main/java/com/umc/owncast/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.umc.owncast.common.config;

import com.umc.owncast.common.jwt.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springdoc.webmvc.core.service.RequestService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -17,6 +17,12 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;


@Configuration
@EnableWebSecurity //기본적인 웹보안 활성화
Expand Down Expand Up @@ -56,4 +62,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}

CorsConfigurationSource apiConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 이후 수정
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
10 changes: 8 additions & 2 deletions src/main/java/com/umc/owncast/common/entity/BaseTimeEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함
@EntityListeners(AuditingEntityListener.class)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public abstract class BaseTimeEntity {

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ public enum ErrorCode implements BaseErrorCode {
OUTPUT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_5000", "서버 출력에 오류가 있습니다. 관리자에게 문의하세요"),

BOOKMARK_NOT_EXIST(HttpStatus.BAD_REQUEST, "BOOKMARK4001", "존재하지 않는 북마크입니다."),
BOOKMARK_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "BOOKMARK4002", "이미 존재하는 북마크입니다.")
BOOKMARK_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "BOOKMARK4002", "이미 존재하는 북마크입니다."),


// 캐스트 관련 에러
REQUEST_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "CAST4001", "캐스트 생성시간이 너무 오래 걸립니다."),

// 기타 에러는 아래에 추가
;

Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/umc/owncast/common/util/StringUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.umc.owncast.common.util;

import java.util.Objects;

public class StringUtil {
public static boolean isBlank(String s){
return Objects.isNull(s) || s.isBlank();
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package com.umc.owncast.domain.cast.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.theokanning.openai.completion.chat.ChatCompletionRequest;
import com.umc.owncast.domain.cast.service.ChatGPTPromptGenerator;
import com.umc.owncast.domain.cast.service.KeywordService;
import com.umc.owncast.common.response.ApiResponse;
import com.umc.owncast.domain.cast.dto.CastSaveDTO;
import com.umc.owncast.domain.cast.dto.CastUpdateDTO;
import com.umc.owncast.domain.cast.dto.KeywordCastCreationDTO;
import com.umc.owncast.domain.cast.dto.ScriptCastCreationDTO;
import com.umc.owncast.domain.cast.service.CastService;
import com.umc.owncast.domain.cast.service.ScriptService;
import com.umc.owncast.domain.cast.service.StreamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.umc.owncast.domain.cast.service.KeywordService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.w3c.dom.stylesheets.LinkStyle;

import java.util.List;

Expand All @@ -21,6 +32,108 @@
@RequestMapping("/api/cast")
public class CastController {
private final KeywordService keywordService;
private final CastService castService;
private final ScriptService scriptService;
private final StreamService streamService;

/* * * * * * * * * * * * * *
* 테스트용 메소드 (나중에 삭제) *
* * * * * * * * * * * * * **/

@PostMapping("/script-test")
@Operation(summary = "스크립트 생성 API (ScriptService 테스트용)")
public String createScript(@Valid @RequestBody KeywordCastCreationDTO castRequest){
System.out.println(castRequest);
return scriptService.createScript(castRequest);
}

/*cast 저장 전 api
@PostMapping("/temporary")
@Operation(summary = "스크립트 생성 api. 저장 버튼 전 화면 입니다.")
public void createCast(@Valid @RequestBody KeywordCastCreationDTO castRequest){
castService.createCast(castRequest);
}*/

/*@GetMapping("/stream-test")
@CrossOrigin(origins = "*") // TODO 프론트 url로 대체
@Operation(summary = "스트리밍 테스트. 테스트용 음악 파일을 스트리밍 합니다")
public Object streamTest(@RequestHeader HttpHeaders headers) throws IOException {
System.out.println("Stream test");
return streamService.stream("test.mp3", headers);
}
@GetMapping("/stream/{filename}")
@CrossOrigin(origins = "*")
@Operation(summary = "filename을 스트리밍합니다")
public Object streamTest(@RequestHeader HttpHeaders headers,
@PathVariable(name = "filename") String filename) throws IOException {
return streamService.stream(filename, headers);
}*/


/* * * * * * * *
* API 용 메소드 *
* * * * * * * **/

/* Cast 생성 API (keyword) */
@PostMapping("/keyword")
@Operation(summary = "키워드로 캐스트를 생성하는 API")
public ApiResponse<Object> createCastByKeyword(@Valid @RequestBody KeywordCastCreationDTO castRequest){
return castService.createCastByKeyword(castRequest);
}

/* Cast 생성 API (script) */
@PostMapping("/script")
@Operation(summary = "스크립트로 캐스트를 생성하는 API.")
public ApiResponse<Object> createCastByScript(@Valid @RequestBody ScriptCastCreationDTO castRequest){
return castService.createCastByScript(castRequest);
}

/* Cast 저장 API */
@PostMapping(value = "/{castId}", consumes = {MediaType.APPLICATION_JSON_VALUE , MediaType.MULTIPART_FORM_DATA_VALUE})
@Operation(summary = "캐스트 저장 API (저장 화면에서 호출)")
public ApiResponse<Object> saveCast(@PathVariable("castId") Long castId,
@Valid @RequestPart(value = "saveInfo") CastSaveDTO saveRequest,
@RequestPart(value = "image", required = false) MultipartFile image){
System.out.println("CastController: save()");
System.out.println(saveRequest);
System.out.println(image);
return castService.saveCast(castId, saveRequest, image);
}

/* Cast 재생 API */
@GetMapping("/{castId}/audio")
@Operation(summary = "캐스트 재생 API")
@CrossOrigin
public ResponseEntity<UrlResource> streamCast(@PathVariable("castId") Long castId,
@RequestHeader HttpHeaders headers){
return castService.streamCast(castId, headers);
}

/* Cast 스크립트 가져오는 API */
@GetMapping("/{castId}/scripts")
@Operation(summary = "캐스트 스크립트 가져오기 API")
public ApiResponse<Object> fetchCastScripts(@PathVariable("castId") Long castId){
return castService.fetchCastScript(castId);
}

/* Cast 수정 API */
@PatchMapping("/{castId}")
@Operation(summary = "캐스트 수정 API")
public ApiResponse<Object> updateCast(@PathVariable("castId") Long castId,
@Valid @RequestPart(value = "updateInfo") CastUpdateDTO updateRequest,
@RequestPart(value = "image", required = false) MultipartFile image){
// TODO 캐스트 생성자 혹은 관리자여야 함
return castService.updateCast(castId, updateRequest, image);
}

/* Cast 삭제 API */
@DeleteMapping("/{castId}")
@Operation(summary = "캐스트 삭제 API")
public ApiResponse<Object> deleteCast(@PathVariable("castId") Long castId){
// TODO 캐스트 생성자 혹은 관리자여야 함
return castService.deleteCast(castId);
}

@GetMapping("/home")
@Operation(summary = "홈 화면 키워드 6개 받아오기")
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/com/umc/owncast/domain/cast/dto/CastDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.umc.owncast.domain.cast.dto;

import com.umc.owncast.domain.cast.entity.Cast;
import com.umc.owncast.domain.enums.Formality;
import com.umc.owncast.domain.sentence.dto.SentenceResponseDTO;
import lombok.*;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CastDTO {
private Long id;
private String title;
private String imagePath;
private String audioLength;
private String voice;
private Formality formality;
private Boolean isPublic;
private Long hits;
private List<SentenceResponseDTO> sentences;

public CastDTO(Cast cast){
id = cast.getId();
title = cast.getTitle();
imagePath = cast.getImagePath();
audioLength = cast.getAudioLength();
voice = cast.getVoice();
formality = cast.getFormality();
isPublic = cast.getIsPublic();
hits = cast.getHits();
sentences = cast.getSentences().stream()
.map(SentenceResponseDTO::new)
.toList();
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/umc/owncast/domain/cast/dto/CastSaveDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.umc.owncast.domain.cast.dto;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.*;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CastSaveDTO {
// 제목 커버이미지 카테고리 공개 여부
@NotEmpty
private String title;

@NotNull
private Long playlistId;

private Boolean isPublic;
}
24 changes: 24 additions & 0 deletions src/main/java/com/umc/owncast/domain/cast/dto/CastScriptDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.umc.owncast.domain.cast.dto;

import com.umc.owncast.domain.cast.entity.Cast;
import com.umc.owncast.domain.sentence.dto.SentenceResponseDTO;
import lombok.*;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CastScriptDTO {
private Long id;
private List<SentenceResponseDTO> sentences;

public CastScriptDTO(Cast cast){
id = cast.getId();
sentences = cast.getSentences().stream()
.map(SentenceResponseDTO::new)
.toList();
}
}
Loading

0 comments on commit f3d3d26

Please sign in to comment.