Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UUID, BufferData> 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<String> 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<String> 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<String> buffer, UUID userId) {
try {
Map<String, Long> wordCounts = buffer.stream()
.filter(Objects::nonNull)
.filter(word -> !word.trim().isEmpty())
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

Optional<Map.Entry<String, Long>> 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<String> 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<Map.Entry<UUID, BufferData>> iterator = userFrameBuffers.entrySet().iterator();
int removedCount = 0;

while (iterator.hasNext()) {
Map.Entry<UUID, BufferData> entry = iterator.next();
if (entry.getValue().lastAccess.isBefore(cutoffTime)) {
iterator.remove();
removedCount++;
}
}

if (removedCount > 0) {
System.out.println("정리된 비활성 버퍼 개수: " + removedCount);
}

System.out.println("현재 활성 버퍼 개수: " + userFrameBuffers.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,19 +25,22 @@ public TranslationController(TranslationService translationService) {
// 수화 번역 API
@PostMapping
public ResponseEntity<Map<String, Object>> translateSignLanguage(@RequestBody RequestSignLanguageDTO requestSignLanguageDTO) {
// 번역된 데이터 가져오기
ResponseTranslationGetDTO responseTranslationGetDTO = translationService.signLanguageTranslation(requestSignLanguageDTO);
TranslationStatusResponse response = translationService.getTranslationStatus(requestSignLanguageDTO);

// 번역 성공 여부 확인
boolean success = responseTranslationGetDTO != null;

// 응답 데이터 구성
Map<String, Object> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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; // 필요한 버퍼 크기
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package HalfFifty.HalfFifty_BE.translation.domain.DTO;

public enum TranslationStatus {
SUCCESS, // 확정된 번역 결과
PROCESSING, // AI 성공했지만 버퍼링 중
FAILED // AI 실패 또는 오류
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,79 @@

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.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;

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) {
Map<String, Object> aiResponse = flaskSignLanguageBean.exec(requestSignLanguageDTO);
public TranslationStatusResponse getTranslationStatus(RequestSignLanguageDTO requestSignLanguageDTO) {
try {
Map<String, Object> 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;
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()) {
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 {
return TranslationStatusResponse.builder()
.status("failed")
.message("수화 인식 실패")
.data(null)
.build();
}

} catch (Exception e) {
return TranslationStatusResponse.builder()
.status("failed")
.message("서버 오류: " + e.getMessage())
.data(null)
.build();
}
}

Expand Down
Loading