From ce07a2b581a8f9703b97507ea44fde0e4c429d61 Mon Sep 17 00:00:00 2001 From: 6suhyeon Date: Fri, 12 Sep 2025 17:08:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=20=EB=8B=A4=EC=88=98=EA=B2=B0=20?= =?UTF-8?q?=EC=8A=A4=EB=AC=B4=EB=94=A9=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HalfFifty_BE/HalfFiftyBeApplication.java | 2 + .../translation/bean/FrameBufferBean.java | 119 ++++++++++++++++++ .../domain/DTO/FrameBufferResult.java | 15 +++ .../service/TranslationService.java | 46 +++++-- 4 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/bean/FrameBufferBean.java create mode 100644 HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/FrameBufferResult.java diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/HalfFiftyBeApplication.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/HalfFiftyBeApplication.java index 7cd35f0..d1bcdeb 100644 --- a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/HalfFiftyBeApplication.java +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/HalfFiftyBeApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class HalfFiftyBeApplication { public static void main(String[] args) { diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/bean/FrameBufferBean.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/bean/FrameBufferBean.java new file mode 100644 index 0000000..8e636e7 --- /dev/null +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/bean/FrameBufferBean.java @@ -0,0 +1,119 @@ +package HalfFifty.HalfFifty_BE.translation.bean; + +import HalfFifty.HalfFifty_BE.translation.domain.DTO.FrameBufferResult; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +public class FrameBufferBean { + private final Map userFrameBuffers = new ConcurrentHashMap<>(); + private final int BUFFER_SIZE = 5; + private final int REQUIRED_MATCHES = 3; + private final int TIMEOUT_MINUTES = 10; // 10분 후 자동 삭제 + + // 버퍼 데이터와 타임스탬프를 함께 저장하는 내부 클래스 + private static class BufferData { + List buffer; + LocalDateTime lastAccess; + + BufferData() { + this.buffer = new ArrayList<>(); + this.lastAccess = LocalDateTime.now(); + } + + void updateAccess() { + this.lastAccess = LocalDateTime.now(); + } + } + + public FrameBufferResult addFrame(UUID userId, String predictedWord) { + if (userId == null || predictedWord == null || predictedWord.trim().isEmpty()) { + return new FrameBufferResult(false, null, 0, BUFFER_SIZE); + } + + BufferData bufferData = userFrameBuffers.computeIfAbsent(userId, k -> new BufferData()); + bufferData.updateAccess(); // 접근 시간 갱신 + + List buffer = bufferData.buffer; + buffer.add(predictedWord.trim()); + + // 버퍼 크기 제한 + if (buffer.size() > BUFFER_SIZE) { + buffer.remove(0); + } + + // 다수결 확인 + if (buffer.size() >= BUFFER_SIZE) { + return checkMajority(buffer, userId); + } + + return new FrameBufferResult(false, null, buffer.size(), BUFFER_SIZE); + } + + private FrameBufferResult checkMajority(List buffer, UUID userId) { + try { + Map wordCounts = buffer.stream() + .filter(Objects::nonNull) + .filter(word -> !word.trim().isEmpty()) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + + Optional> mostFrequent = wordCounts.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (mostFrequent.isPresent() && mostFrequent.get().getValue() >= REQUIRED_MATCHES) { + // 확정되면 해당 사용자의 버퍼 초기화 + clearBuffer(userId); + return new FrameBufferResult(true, mostFrequent.get().getKey(), BUFFER_SIZE, BUFFER_SIZE); + } + + return new FrameBufferResult(false, null, buffer.size(), BUFFER_SIZE); + + } catch (Exception e) { + System.err.println("Error in checkMajority: " + e.getMessage()); + return new FrameBufferResult(false, null, buffer.size(), BUFFER_SIZE); + } + } + + public void clearBuffer(UUID userId) { + if (userId != null) { + userFrameBuffers.remove(userId); + } + } + + public List getBuffer(UUID userId) { + if (userId == null) { + return new ArrayList<>(); + } + BufferData bufferData = userFrameBuffers.get(userId); + return bufferData != null ? new ArrayList<>(bufferData.buffer) : new ArrayList<>(); + } + + // 메모리 누수 방지: 10분마다 오래된 버퍼 자동 삭제 + @Scheduled(fixedRate = 600000) // 10분마다 실행 + public void cleanupOldBuffers() { + LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(TIMEOUT_MINUTES); + + Iterator> iterator = userFrameBuffers.entrySet().iterator(); + int removedCount = 0; + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().lastAccess.isBefore(cutoffTime)) { + iterator.remove(); + removedCount++; + } + } + + if (removedCount > 0) { + System.out.println("정리된 비활성 버퍼 개수: " + removedCount); + } + + System.out.println("현재 활성 버퍼 개수: " + userFrameBuffers.size()); + } +} \ No newline at end of file diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/FrameBufferResult.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/FrameBufferResult.java new file mode 100644 index 0000000..9298cc9 --- /dev/null +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/FrameBufferResult.java @@ -0,0 +1,15 @@ +package HalfFifty.HalfFifty_BE.translation.domain.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class FrameBufferResult { + private boolean confirmed; // 확정 여부 + private String confirmedWord; // 확정된 단어 + private int currentBufferSize; // 현재 버퍼 크기 + private int requiredBufferSize; // 필요한 버퍼 크기 +} diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java index 19bfba2..604d363 100644 --- a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java @@ -2,7 +2,9 @@ import HalfFifty.HalfFifty_BE.translation.bean.DeleteTranslationBean; import HalfFifty.HalfFifty_BE.translation.bean.FlaskSignLanguageBean; +import HalfFifty.HalfFifty_BE.translation.bean.FrameBufferBean; import HalfFifty.HalfFifty_BE.translation.bean.SaveTranslationBean; +import HalfFifty.HalfFifty_BE.translation.domain.DTO.FrameBufferResult; import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestSignLanguageDTO; import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestTranslationDeleteDTO; import HalfFifty.HalfFifty_BE.translation.domain.DTO.ResponseTranslationGetDTO; @@ -10,31 +12,57 @@ import org.springframework.stereotype.Service; import java.util.Map; +import java.util.UUID; @Service public class TranslationService { - SaveTranslationBean saveTranslationBean; - FlaskSignLanguageBean flaskSignLanguageBean; - DeleteTranslationBean deleteTranslationBean; + private final SaveTranslationBean saveTranslationBean; + private final FlaskSignLanguageBean flaskSignLanguageBean; + private final DeleteTranslationBean deleteTranslationBean; + private final FrameBufferBean frameBufferBean; @Autowired - public TranslationService(SaveTranslationBean saveTranslationBean, FlaskSignLanguageBean flaskSignLanguageBean, DeleteTranslationBean deleteTranslationBean) { + public TranslationService(SaveTranslationBean saveTranslationBean, + FlaskSignLanguageBean flaskSignLanguageBean, + DeleteTranslationBean deleteTranslationBean, + FrameBufferBean frameBufferBean) { this.saveTranslationBean = saveTranslationBean; this.flaskSignLanguageBean = flaskSignLanguageBean; this.deleteTranslationBean = deleteTranslationBean; + this.frameBufferBean = frameBufferBean; } + // 기존 메서드 시그니처 그대로 유지하면서 내부적으로 버퍼링 처리 public ResponseTranslationGetDTO signLanguageTranslation(RequestSignLanguageDTO requestSignLanguageDTO) { + // AI 서버에서 예측 결과 받기 Map aiResponse = flaskSignLanguageBean.exec(requestSignLanguageDTO); if (aiResponse != null && Boolean.TRUE.equals(aiResponse.get("success"))) { - String translatedWord = (String) aiResponse.get("predicted_label"); - Double probability = (Double) aiResponse.get("confidence"); - return saveTranslationBean.exec(requestSignLanguageDTO.getUserId(), translatedWord, probability); - } else { - return null; + String predictedWord = (String) aiResponse.get("predicted_label"); + Double confidence = (Double) aiResponse.get("confidence"); + + // 프레임 버퍼에 추가하고 다수결 확인 + FrameBufferResult bufferResult = frameBufferBean.addFrame( + requestSignLanguageDTO.getUserId(), + predictedWord + ); + + // 확정된 경우에만 저장하고 반환 + if (bufferResult.isConfirmed()) { + return saveTranslationBean.exec( + requestSignLanguageDTO.getUserId(), + bufferResult.getConfirmedWord(), + confidence + ); + } else { + // 아직 확정되지 않은 경우 null 반환 + // -> 프론트엔드에서는 기존처럼 "번역 실패"로 처리됨 + return null; + } } + + return null; } // 번역 기록 삭제 From b13299d6f0e78d449f634de0a89064a38e123e51 Mon Sep 17 00:00:00 2001 From: 6suhyeon Date: Wed, 17 Sep 2025 15:08:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat=20:=20=EB=8B=A4=EC=88=98=EA=B2=B0=20?= =?UTF-8?q?=EC=8A=A4=EB=AC=B4=EB=94=A9=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TranslationController.java | 26 ++++--- .../domain/DTO/TranslationStatus.java | 7 ++ .../domain/DTO/TranslationStatusResponse.java | 12 ++++ .../service/TranslationService.java | 67 +++++++++++-------- 4 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatus.java create mode 100644 HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatusResponse.java diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/controller/TranslationController.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/controller/TranslationController.java index 2c3064c..034ef40 100644 --- a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/controller/TranslationController.java +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/controller/TranslationController.java @@ -3,6 +3,7 @@ import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestSignLanguageDTO; import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestTranslationDeleteDTO; import HalfFifty.HalfFifty_BE.translation.domain.DTO.ResponseTranslationGetDTO; +import HalfFifty.HalfFifty_BE.translation.domain.DTO.TranslationStatusResponse; import HalfFifty.HalfFifty_BE.translation.service.TranslationService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -24,19 +25,22 @@ public TranslationController(TranslationService translationService) { // 수화 번역 API @PostMapping public ResponseEntity> translateSignLanguage(@RequestBody RequestSignLanguageDTO requestSignLanguageDTO) { - // 번역된 데이터 가져오기 - ResponseTranslationGetDTO responseTranslationGetDTO = translationService.signLanguageTranslation(requestSignLanguageDTO); + TranslationStatusResponse response = translationService.getTranslationStatus(requestSignLanguageDTO); - // 번역 성공 여부 확인 - boolean success = responseTranslationGetDTO != null; - - // 응답 데이터 구성 Map responseMap = new HashMap<>(); - responseMap.put("success", success); - responseMap.put("message", success ? "수화 번역 성공" : "수화 번역 실패"); - responseMap.put("translationId", success ? responseTranslationGetDTO.getTranslationId() : null); - responseMap.put("translatedWord", success ? responseTranslationGetDTO.getTranslationWord() : null); - responseMap.put("probability", success ? responseTranslationGetDTO.getProbability() : null); + responseMap.put("success", "success".equals(response.getStatus())); + responseMap.put("message", response.getMessage()); + responseMap.put("status", response.getStatus()); + responseMap.put("translationId", null); + responseMap.put("translatedWord", null); + responseMap.put("probability", null); + + if ("success".equals(response.getStatus()) && response.getData() != null) { + ResponseTranslationGetDTO data = response.getData(); + responseMap.put("translationId", data.getTranslationId()); + responseMap.put("translatedWord", data.getTranslationWord()); + responseMap.put("probability", data.getProbability()); + } return ResponseEntity.status(HttpStatus.OK).body(responseMap); } diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatus.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatus.java new file mode 100644 index 0000000..8a80222 --- /dev/null +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatus.java @@ -0,0 +1,7 @@ +package HalfFifty.HalfFifty_BE.translation.domain.DTO; + +public enum TranslationStatus { + SUCCESS, // 확정된 번역 결과 + PROCESSING, // AI 성공했지만 버퍼링 중 + FAILED // AI 실패 또는 오류 +} diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatusResponse.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatusResponse.java new file mode 100644 index 0000000..40ba199 --- /dev/null +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/domain/DTO/TranslationStatusResponse.java @@ -0,0 +1,12 @@ +package HalfFifty.HalfFifty_BE.translation.domain.DTO; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TranslationStatusResponse { + private String status; + private String message; + private ResponseTranslationGetDTO data; +} \ No newline at end of file diff --git a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java index 604d363..9a51252 100644 --- a/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java +++ b/HalfFifty_BE/src/main/java/HalfFifty/HalfFifty_BE/translation/service/TranslationService.java @@ -4,10 +4,7 @@ import HalfFifty.HalfFifty_BE.translation.bean.FlaskSignLanguageBean; import HalfFifty.HalfFifty_BE.translation.bean.FrameBufferBean; import HalfFifty.HalfFifty_BE.translation.bean.SaveTranslationBean; -import HalfFifty.HalfFifty_BE.translation.domain.DTO.FrameBufferResult; -import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestSignLanguageDTO; -import HalfFifty.HalfFifty_BE.translation.domain.DTO.RequestTranslationDeleteDTO; -import HalfFifty.HalfFifty_BE.translation.domain.DTO.ResponseTranslationGetDTO; +import HalfFifty.HalfFifty_BE.translation.domain.DTO.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -33,36 +30,52 @@ public TranslationService(SaveTranslationBean saveTranslationBean, this.frameBufferBean = frameBufferBean; } - // 기존 메서드 시그니처 그대로 유지하면서 내부적으로 버퍼링 처리 - public ResponseTranslationGetDTO signLanguageTranslation(RequestSignLanguageDTO requestSignLanguageDTO) { - // AI 서버에서 예측 결과 받기 - Map aiResponse = flaskSignLanguageBean.exec(requestSignLanguageDTO); + public TranslationStatusResponse getTranslationStatus(RequestSignLanguageDTO requestSignLanguageDTO) { + try { + Map aiResponse = flaskSignLanguageBean.exec(requestSignLanguageDTO); - if (aiResponse != null && Boolean.TRUE.equals(aiResponse.get("success"))) { - String predictedWord = (String) aiResponse.get("predicted_label"); - Double confidence = (Double) aiResponse.get("confidence"); + if (aiResponse != null && Boolean.TRUE.equals(aiResponse.get("success"))) { + String predictedWord = (String) aiResponse.get("predicted_label"); + Double confidence = (Double) aiResponse.get("confidence"); - // 프레임 버퍼에 추가하고 다수결 확인 - FrameBufferResult bufferResult = frameBufferBean.addFrame( - requestSignLanguageDTO.getUserId(), - predictedWord - ); - - // 확정된 경우에만 저장하고 반환 - if (bufferResult.isConfirmed()) { - return saveTranslationBean.exec( + FrameBufferResult bufferResult = frameBufferBean.addFrame( requestSignLanguageDTO.getUserId(), - bufferResult.getConfirmedWord(), - confidence + predictedWord ); + + if (bufferResult.isConfirmed()) { + ResponseTranslationGetDTO result = saveTranslationBean.exec( + requestSignLanguageDTO.getUserId(), + bufferResult.getConfirmedWord(), + confidence + ); + return TranslationStatusResponse.builder() + .status("success") + .message("수화 번역 성공") + .data(result) + .build(); + } else { + return TranslationStatusResponse.builder() + .status("processing") + .message("수화 인식 중... (" + bufferResult.getCurrentBufferSize() + "/5)") + .data(null) + .build(); + } } else { - // 아직 확정되지 않은 경우 null 반환 - // -> 프론트엔드에서는 기존처럼 "번역 실패"로 처리됨 - return null; + return TranslationStatusResponse.builder() + .status("failed") + .message("수화 인식 실패") + .data(null) + .build(); } - } - return null; + } catch (Exception e) { + return TranslationStatusResponse.builder() + .status("failed") + .message("서버 오류: " + e.getMessage()) + .data(null) + .build(); + } } // 번역 기록 삭제