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
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ dependencies {
// https://mvnrepository.com/artifact/org.springframework/spring-webmvc
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'



//Webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'

Expand All @@ -67,6 +69,12 @@ dependencies {
// Google Cloud Vision
implementation 'com.google.cloud:google-cloud-vision:3.61.0'

// Google cloud speech
implementation 'com.google.cloud:google-cloud-speech:4.39.0'

//Google API


// Logstash Logback Encoder
implementation("net.logstash.logback:logstash-logback-encoder:8.0")
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/hongik/Todoing/domain/todo/domain/Todo.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class Todo extends BaseEntity {
@Column(name = "label_id", nullable = false)
private Long labelId;

@Column(name = "verification_id", nullable = false)
@Column(name = "verification_id", nullable = true)
private Long verification_id;

@OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import hongik.Todoing.domain.verification.domain.Verification;
import hongik.Todoing.domain.verification.dto.TextAnnotationDto;
import hongik.Todoing.domain.verification.dto.VerificationResponse;
import hongik.Todoing.domain.verification.service.SpeechService;
import hongik.Todoing.domain.verification.service.VerificationService;
import hongik.Todoing.domain.verification.service.VisionService;
import hongik.Todoing.global.apiPayload.ApiResponse;
Expand All @@ -25,6 +26,7 @@ public class TodoVerificationController {

private final VisionService visionService;
private final VerificationService verificationService;
private final SpeechService speechService;

/*
- 사진으로 인증하기 -> 1. 사진 업로드 -> 2. 사진에서 글자 인식 -> 3. 인식된 글자가 할 일의 인증 문구와 일치하는지 확인
Expand Down Expand Up @@ -56,7 +58,7 @@ public ApiResponse<?> verifyTextTest(@RequestPart("image")MultipartFile image) t
return ApiResponse.onSuccess(result);
}

@Operation(summary = "사진 인증 - 라벨 감지 테스트 컨트롤러")
@Operation(summary = "[x]사진 인증 - 라벨 감지 테스트 컨트롤러")
@PostMapping("/test/labels")
public ApiResponse<?> detectLabelsTest(@RequestPart("image")MultipartFile image) throws IOException {
List<EntityAnnotation> labels = visionService.detectLabels(image);
Expand All @@ -70,7 +72,7 @@ public ApiResponse<?> detectLabelsTest(@RequestPart("image")MultipartFile image)
return ApiResponse.onSuccess(labelDescriptions);
}

@Operation(summary = "사진 인증, 글과 사진 어떤 것이든 사진은 한 번에 묶어서 인증이 가능합니다.")
@Operation(summary = "[x]사진 인증, 글과 사진 어떤 것이든 사진은 한 번에 묶어서 인증이 가능합니다.")
@PostMapping("/image")
public ApiResponse<?> verifyTodoImage(
@AuthenticationPrincipal PrincipalDetails principal,
Expand All @@ -81,4 +83,40 @@ public ApiResponse<?> verifyTodoImage(
return ApiResponse.onSuccess(response);
}

@Operation(summary = "음성 인증 - STT 결과 transcript를 기반으로 투두 인증")
@PostMapping("/voice")
public ApiResponse<?> verifyTodoVoice(
@AuthenticationPrincipal PrincipalDetails principal,
@RequestParam Long todoId,
@RequestParam String transcript
) {
VerificationResponse response =
verificationService.verifyTodoVoice(principal.getMember(), todoId, transcript);
return ApiResponse.onSuccess(response);
}

@Operation(summary = "텍스트 인증 - 사용자가 작성한 텍스트(50자 내외)로 투두 인증")
@PostMapping("/text")
public ApiResponse<?> verifyTodoText(
@AuthenticationPrincipal PrincipalDetails principal,
@RequestParam Long todoId,
@RequestParam String text
) {
VerificationResponse response =
verificationService.verifyTodoText(principal.getMember(), todoId, text);
return ApiResponse.onSuccess(response);
}

@PostMapping("/voice/file")
public ApiResponse<?> verifyTodoVoiceFile(
@AuthenticationPrincipal PrincipalDetails principal,
@RequestParam Long todoId,
@RequestPart("audio") MultipartFile audioFile
) throws IOException {
String transcript = speechService.stt(audioFile); // 음성 → 텍스트
VerificationResponse response =
verificationService.verifyTodoVoice(principal.getMember(), todoId, transcript);
return ApiResponse.onSuccess(response);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class Verification {
@Enumerated(EnumType.STRING)
private VerificationType type;

private Boolean success;

private double confidence;

@Column(name = "todo_id")
private Long todoId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@ public class VerificationUsage extends BaseEntity {

@Column(name = "user_id")
private Long userId;

public void increase() {
if (usageCount == null) {
usageCount = 0;
}
usageCount++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hongik.Todoing.domain.verification.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class VerificationResult {
private final boolean success;
private final double confidence;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hongik.Todoing.domain.verification.repository;

import hongik.Todoing.domain.verification.domain.VerificationUsage;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface VerificationUsageRepository extends JpaRepository<VerificationUsage, Long> {

Optional<VerificationUsage> findByUserId(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package hongik.Todoing.domain.verification.service;

import com.google.cloud.speech.v1.*;
import com.google.protobuf.ByteString;
import hongik.Todoing.global.common.AuditingAi.AuditingAI;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
@Slf4j
public class SpeechService {
// 음성을 텍스트로 변환하는 서비스 (STT)
@AuditingAI("음성을 통한 텍스트 변환 호출기")
public String stt(MultipartFile audioFile) throws IOException {

// 1. 파일을 ByteString으로 변환
ByteString audioBytes = ByteString.readFrom(audioFile.getInputStream());

// 2. Google Speech API용 Audio 객체 생성
RecognitionAudio audio = RecognitionAudio.newBuilder()
.setContent(audioBytes)
.build();

// 3. 설정 (한국어 예시: ko-KR)
RecognitionConfig config = RecognitionConfig.newBuilder()
.setEncoding(RecognitionConfig.AudioEncoding.LINEAR16) // 파일 인코딩에 맞게 조정 필요
.setLanguageCode("ko-KR")
.build();

// 4. 클라이언트 생성 후 요청
try (SpeechClient speechClient = SpeechClient.create()) {
RecognizeRequest request = RecognizeRequest.newBuilder()
.setConfig(config)
.setAudio(audio)
.build();

RecognizeResponse response = speechClient.recognize(request);

StringBuilder transcriptBuilder = new StringBuilder();

for (SpeechRecognitionResult result : response.getResultsList()) {
SpeechRecognitionAlternative alternative = result.getAlternativesList().get(0);
transcriptBuilder.append(alternative.getTranscript()).append(" ");
}

String transcript = transcriptBuilder.toString().trim();
log.info("STT transcript = {}", transcript);
return transcript;
}
}
}
Loading