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: OCR, STT 기능 개선 및 버그 수정 #32

Merged
merged 26 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f342234
Feat: POST 저장 기능, OCR 구현, 폴더 수정, 페이지별 조회 기능 구현 및 배포 (#66)
yugyeom-ghim Oct 25, 2024
d0e1294
Feat: AI서버에서 STT 결과 응답 왔을때 STT 결과 처리하는 기능 구현 (#27)
yugyeom-ghim Nov 1, 2024
4e8c2f8
Chore: 배포 자동화 및 Slack Webhook 설정 (#29)
yugyeom-ghim Nov 1, 2024
89dfb1e
Refactor: Document, Folder 이슈 해결 (#28)
yunjunghun0116 Nov 1, 2024
3aa5fe1
Chore: 0.0.3 버전 배포
yugyeom-ghim Nov 1, 2024
75553f3
Feat: 스웨거에서 multipart-formdata 보낼 수 있는 기능 구현
yugyeom-ghim Nov 3, 2024
114e23d
Chore: 서브모듈 설정파일 tesseract 경로 변경
yugyeom-ghim Nov 3, 2024
5de17fa
Fix: OCR content columnDefinition의 text를 longtext로 변경
yugyeom-ghim Nov 3, 2024
dd0dde5
Chore: 버전 0.0.3으로 변경
yugyeom-ghim Nov 3, 2024
79647f6
Fix: 오디오 파일 이름 정규표현식 제거
hynseoj Nov 4, 2024
8d55a03
fix: AI서버 stt요청 multipart formdata 오류 해결
yugyeom-ghim Nov 4, 2024
5071eb1
fix: InputStreamResource를 ByteArrayResource로 변경하여 스트림 재사용 문제 해결
yugyeom-ghim Nov 4, 2024
83bde51
Fix: AI서버에서의 결과에 토큰이 필요하지 않도록 수정
yugyeom-ghim Nov 5, 2024
3080d1d
Fix: AI서버 api 명세에 맞게 request dto 수정
yugyeom-ghim Nov 5, 2024
bf3c18c
Fix: 페이지 넘김 테이블 녹음 종료 시간 nullable 하게 수정
hynseoj Nov 5, 2024
a0a6c69
Fix: ai 기능 요청에서 페이지 번호 0 가능하도록 수정
hynseoj Nov 5, 2024
8aa7ff2
Fix: ai 기능 요청시 Summary, Problem 을 cascade 옵션 없이 각각 영속화하도록 수정
hynseoj Nov 5, 2024
16422aa
Fix: ai 기능 재요청 관련 트랜잭션 수정
hynseoj Nov 6, 2024
3e0b2e7
Chore: STT 로깅 설정
yugyeom-ghim Nov 6, 2024
8b55b75
Chore: recording 디버깅을 위해 exception message 형태 변경
yugyeom-ghim Nov 6, 2024
c6e0441
Fix: filePath 컬럼 제한 길이를 넘어 생기는 오류 수정
yugyeom-ghim Nov 6, 2024
93a3b68
Test: 로직 변경으로 인한 테스트코드 인자 수정
yugyeom-ghim Nov 6, 2024
15370f5
Fix: 페이지 넘김 이벤트의 EndTime이 null일때 예외처리 추가
yugyeom-ghim Nov 6, 2024
934196b
Fix: annotation의 pageNumber가 양수인 조건에서 0이상으로 변경
yugyeom-ghim Nov 6, 2024
4e756a0
Fix : delete 204 -> 200으로 변경
mingjuu Nov 7, 2024
fa22fc1
Merge remote-tracking branch 'origin/Weekly10' into release/0.0.3
yugyeom-ghim Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "Team29_BE_Submodule"]
path = Team29_BE_Submodule
url = [email protected]:29ana-notai/Team29_BE_Submodule.git
branch = release/0.0.2
branch = release/0.0.3
5 changes: 0 additions & 5 deletions Dockerfile

This file was deleted.

2 changes: 1 addition & 1 deletion Team29_BE_Submodule
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'notai'
version = '0.0.2'
version = '0.0.3'

java {
toolchain {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@ public ResponseEntity<Void> deleteAnnotation(
) {

annotationService.deleteAnnotation(documentId, annotationId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
return new ResponseEntity<>(HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
package notai.annotation.presentation.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;

public record CreateAnnotationRequest(

@Positive(message = "페이지 번호는 양수여야 합니다.")
@PositiveOrZero(message = "페이지 번호는 0 이상이어야 합니다.")
int pageNumber,

// @Max(value = ?, message = "x 좌표는 최대 ? 이하여야 합니다.")
// @Max(value = ?, message = "x 좌표는 최대 ? 이하여야 합니다.")
@PositiveOrZero(message = "x 좌표는 0 이상이어야 합니다.")
int x,

// @Max(value = ?, message = "x 좌표는 최대 ? 이하여야 합니다.")
// @Max(value = ?, message = "x 좌표는 최대 ? 이하여야 합니다.")
@PositiveOrZero(message = "y 좌표는 0 이상이어야 합니다.")
int y,

// @Max(value = ?, message = "width는 최대 ? 이하여야 합니다.")
// @Max(value = ?, message = "width는 최대 ? 이하여야 합니다.")
@Positive(message = "width는 양수여야 합니다.")
int width,

// @Max(value = ?, message = "height는 최대 ? 이하여야 합니다.")
// @Max(value = ?, message = "height는 최대 ? 이하여야 합니다.")
@Positive(message = "height는 양수여야 합니다.")
int height,

String content
) {}
) {
}
6 changes: 3 additions & 3 deletions src/main/java/notai/client/ai/AiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import notai.client.ai.request.LlmTaskRequest;
import notai.client.ai.response.TaskResponse;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.service.annotation.PostExchange;

import java.io.InputStream;

public interface AiClient {

@PostExchange(url = "/api/ai/llm")
TaskResponse submitLlmTask(@RequestBody LlmTaskRequest request);

@PostExchange(url = "/api/ai/stt", contentType = MediaType.MULTIPART_FORM_DATA_VALUE)
TaskResponse submitSttTask(@RequestBody InputStream audioFileStream);
TaskResponse submitSttTask(@RequestPart("audio") ByteArrayResource audioFile);
}
32 changes: 23 additions & 9 deletions src/main/java/notai/client/ai/AiClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;

import static notai.client.HttpInterfaceUtil.createHttpInterface;
import static notai.common.exception.ErrorMessages.AI_SERVER_ERROR;
Expand All @@ -20,15 +23,26 @@ public class AiClientConfig {

@Bean
public AiClient aiClient() {
RestClient restClient =
RestClient.builder().baseUrl(aiServerUrl).requestInterceptor((request, body, execution) -> {
request.getHeaders().setContentLength(body.length); // Content-Length 설정 안하면 411 에러 발생
return execution.execute(request, body);
}).defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
String responseBody = new String(response.getBody().readAllBytes());
log.error("Response Status: {}", response.getStatusCode());
throw new ExternalApiException(AI_SERVER_ERROR, response.getStatusCode().value());
}).build();
RestClient restClient = RestClient.builder()
.baseUrl(aiServerUrl)
.messageConverters(converters -> {
converters.addAll(new RestTemplate().getMessageConverters());
converters.add(new FormHttpMessageConverter());
})
.requestInterceptor((request, body, execution) -> {
request.getHeaders().setContentLength(body.length); // Content-Length 설정 안하면 411 에러 발생
return execution.execute(request, body);
})
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
String responseBody = new String(response.getBody().readAllBytes());
log.error("AI 서버에서 오류가 발생했습니다. - Status: {}, Body: {}",
response.getStatusCode(),
responseBody
);
throw new ExternalApiException(AI_SERVER_ERROR, response.getStatusCode().value());
})
.build();

return createHttpInterface(restClient, AiClient.class);
}
}
8 changes: 6 additions & 2 deletions src/main/java/notai/common/config/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ public class AuthConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/api/**").excludePathPatterns(
"/api/members/oauth/login/**").excludePathPatterns("/api/members/token/refresh");
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/members/oauth/login/**")
.excludePathPatterns("/api/members/token/refresh")
.excludePathPatterns("/api/ai/stt/callback")
.excludePathPatterns("/api/ai/llm/callback");
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/notai/common/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ public OpenAPI openAPI() {
}

private Info apiInfo() {
return new Info().title("notai API").description("notai API 문서입니다.").version("0.0.2");
return new Info().title("notai API").description("notai API 문서입니다.").version("0.0.3");
}
}
7 changes: 1 addition & 6 deletions src/main/java/notai/common/domain/vo/FilePath.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,14 @@
@NoArgsConstructor(access = PROTECTED)
public class FilePath {

@Column(length = 50)
@Column(length = 255)
private String filePath;

private FilePath(String filePath) {
this.filePath = filePath;
}

public static FilePath from(String filePath) {
// 추후 확장자 추가
if (!filePath.matches(
"[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣()\\[\\]+\\-&/_\\s]+(/[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣()\\[\\]+\\-&/_\\s]+)*/?[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣()\\[\\]+\\-&/_\\s]+\\.mp3")) {
throw new BadRequestException(INVALID_FILE_TYPE);
}
return new FilePath(filePath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ public class BadRequestException extends ApplicationException {
public BadRequestException(ErrorMessages message) {
super(message, 400);
}
public BadRequestException(String message) {
super(message, 400);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package notai.document.presentation;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import lombok.RequiredArgsConstructor;
import notai.auth.Auth;
import notai.document.application.DocumentQueryService;
Expand All @@ -12,6 +14,7 @@
import notai.document.presentation.response.DocumentFindResponse;
import notai.document.presentation.response.DocumentSaveResponse;
import notai.document.presentation.response.DocumentUpdateResponse;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -30,10 +33,11 @@ public class DocumentController {
private static final Long ROOT_FOLDER_ID = -1L;
private static final String FOLDER_URL_FORMAT = "/api/folders/%s/documents/%s";

@PostMapping
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<DocumentSaveResponse> saveDocument(
@Auth Long memberId,
@PathVariable Long folderId,
@Parameter(content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE))
@RequestPart MultipartFile pdfFile,
@RequestPart DocumentSaveRequest documentSaveRequest
) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/notai/llm/application/LlmTaskService.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,16 @@ private void submitPageTask(Integer pageNumber, Map<Integer, List<Annotation>> a
if (foundSummary.isEmpty() && foundProblem.isEmpty()) {
Summary summary = new Summary(foundDocument, pageNumber);
Problem problem = new Problem(foundDocument, pageNumber);
summaryRepository.save(summary);
problemRepository.save(problem);

LlmTask taskRecord = new LlmTask(taskId, summary, problem);
llmTaskRepository.save(taskRecord);
}
if (foundSummary.isPresent() && foundProblem.isPresent()) {
LlmTask foundTaskRecord = llmTaskRepository.getBySummaryAndProblem(foundSummary.get(), foundProblem.get());
llmTaskRepository.delete(foundTaskRecord);
llmTaskRepository.flush();

LlmTask taskRecord = new LlmTask(taskId, foundSummary.get(), foundProblem.get());
llmTaskRepository.save(taskRecord);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/notai/llm/domain/LlmTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public class LlmTask extends RootEntity<UUID> {
private UUID id;

@NotNull
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "summary_id")
private Summary summary;

@NotNull
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "problem_id")
private Problem problem;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package notai.llm.presentation.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import notai.llm.application.command.LlmTaskSubmitCommand;

import java.util.List;
Expand All @@ -10,7 +10,7 @@ public record LlmTaskSubmitRequest(

@NotNull(message = "문서 ID는 필수 입력 값입니다.") Long documentId,

List<@Positive(message = "페이지 번호는 양수여야 합니다.") Integer> pages
List<@PositiveOrZero(message = "페이지 번호는 음수일 수 없습니다.") Integer> pages
) {
public LlmTaskSubmitCommand toCommand() {
return new LlmTaskSubmitCommand(documentId, pages);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/notai/ocr/domain/OCR.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class OCR extends RootEntity<Long> {
private Integer pageNumber;

@NotNull
@Column(name = "content", columnDefinition = "TEXT")
@Column(name = "content", columnDefinition = "LONGTEXT")
private String content;

public OCR(Document document, Integer pageNumber, String content) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public class PageRecording {
@NotNull
private Double startTime;

@NotNull
private Double endTime;

public PageRecording(Recording recording, Integer pageNumber, Double startTime, Double endTime) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public RecordingSaveResult saveRecording(RecordingSaveCommand command) {
return RecordingSaveResult.of(savedRecording.getId(), foundDocument.getId(), savedRecording.getCreatedAt());

} catch (IllegalArgumentException e) {
throw new BadRequestException(INVALID_AUDIO_ENCODING);
throw new BadRequestException(INVALID_AUDIO_ENCODING + " : " + e.getMessage());
} catch (IOException e) {
throw new InternalServerErrorException(FILE_SAVE_ERROR); // TODO: 재시도 로직 추가?
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/notai/stt/application/SttService.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void updateSttResult(UpdateSttResultCommand command) {
SttTask sttTask = sttTaskRepository.getById(command.taskId());
Stt stt = sttTask.getStt();
Recording recording = stt.getRecording();

List<PageRecording> pageRecordings = pageRecordingRepository.findAllByRecordingIdOrderByStartTime(recording.getId());

SttPageMatchedDto matchedResult = stt.matchWordsWithPages(command.words(), pageRecordings);
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/notai/stt/application/SttTaskService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
import notai.stt.domain.SttRepository;
import notai.sttTask.domain.SttTask;
import notai.sttTask.domain.SttTaskRepository;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;

@Service
@Transactional
Expand All @@ -35,8 +36,17 @@ public void submitSttTask(SttRequestCommand command) {
Recording recording = recordingRepository.getById(command.recordingId());
File audioFile = validateAudioFile(command.audioFilePath());

try (FileInputStream fileInputStream = new FileInputStream(audioFile)) {
TaskResponse response = aiClient.submitSttTask(fileInputStream);
try {
byte[] audioBytes = Files.readAllBytes(audioFile.toPath());

ByteArrayResource resource = new ByteArrayResource(audioBytes) {
@Override
public String getFilename() {
return audioFile.getName();
}
};

TaskResponse response = aiClient.submitSttTask(resource);
createAndSaveSttTask(recording, response);
} catch (IOException e) {
throw new FileProcessException(FILE_READ_ERROR);
Expand Down
21 changes: 10 additions & 11 deletions src/main/java/notai/stt/domain/Stt.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public SttPageMatchedDto matchWordsWithPages(
for (PageRecording page : pageRecordings) {
List<SttPageMatchedDto.PageMatchedWord> pageWords = new ArrayList<>();
double pageStart = page.getStartTime();
double pageEnd = page.getEndTime();
Double pageEnd = page.getEndTime();

// 현재 페이지의 시간 범위에 속하는 단어들을 찾아 매칭
while (wordIndex < words.size()) {
Expand All @@ -104,18 +104,17 @@ public SttPageMatchedDto matchWordsWithPages(
continue;
}

// 마지막 페이지가 아닐 경우, 페이지 종료 시간을 벗어난 단어가 나오면 다음 페이지로
if (page != lastPage && word.start() - TIME_THRESHOLD >= pageEnd) {
// 마지막 페이지이거나 endTime이 null이면 시작 시간만 체크
if ((page == lastPage || pageEnd == null) || word.start() - TIME_THRESHOLD < pageEnd) {
pageWords.add(new SttPageMatchedDto.PageMatchedWord(
word.word(),
(int) word.start(),
(int) word.end()
));
wordIndex++;
} else {
break;
}

// 현재 페이지에 단어 매칭하여 추가
pageWords.add(new SttPageMatchedDto.PageMatchedWord(
word.word(),
(int) word.start(),
(int) word.end()
));
wordIndex++;
}

// 매칭된 단어가 있는 경우만 맵에 추가
Expand Down
Loading
Loading