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
@@ -0,0 +1,50 @@
package com.bamboo.log.emotion.controller;

import com.bamboo.log.emotion.dto.BoundingBox;
import com.bamboo.log.emotion.dto.EmotionAnalysisResponse;
import com.bamboo.log.emotion.dto.req.EmotionAnalysisRequest;
import com.bamboo.log.emotion.service.EmotionAnalysisService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/emotion")
@RequiredArgsConstructor
@Tag(name = "Emotion Analysis", description = "얼굴 감정(표정)분석 API")
public class EmotionController {

private final EmotionAnalysisService emotionAnalysisService;

@PostMapping(value = "/result", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "감정 분석",
description = "반환받은 얼굴 영역 좌표와 이미지를 분석하여 감정을 반환합니다."
)
public ResponseEntity<EmotionAnalysisResponse> analyzeEmotion(

@RequestPart("image")
@Parameter(description = "감정 분석할 얼굴 이미지", required = true)
MultipartFile image,

@RequestPart("faceBox")
@Parameter(description = "얼굴 영역 좌표 (JSON 형식)", required = true, example = "{\"x1\": 12.34, \"y1\": 56.78, \"x2\": 123.456, \"y2\": 789.87}")
BoundingBox faceBox) {

EmotionAnalysisRequest request = new EmotionAnalysisRequest(image, faceBox);
EmotionAnalysisResponse response = emotionAnalysisService.analyzeEmotion(request);

HttpStatus status = HttpStatus.resolve(response.status().value());
if (status == null) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
}

return ResponseEntity.status(status).body(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/emotion")
@Tag(name = "Face Detection", description = "얼굴 인식 관련 API")
@RequestMapping("/api/face")
@Tag(name = "Face Detection", description = "얼굴 인식 API")
public class FaceDetectController {
private final FaceDetectionService faceDetectionService;

@PostMapping("/detect")
@Operation(
summary = "얼굴 인식 API",
summary = "얼굴 인식",
description = "이미지를 업로드하면 얼굴 인식 여부를 반환합니다."
)
public ResponseEntity<FaceDetectionResponse> detectFace(@RequestParam("image") MultipartFile image) {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/bamboo/log/emotion/domain/EmotionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bamboo.log.emotion.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum EmotionType {
ANGRY, HAPPY, NEUTRAL, SAD, NONE ;
}
3 changes: 3 additions & 0 deletions src/main/java/com/bamboo/log/emotion/dto/BoundingBox.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.bamboo.log.emotion.dto;

public record BoundingBox(double x1, double y1, double x2, double y2) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bamboo.log.emotion.dto;

import com.bamboo.log.emotion.domain.EmotionType;
import org.springframework.http.HttpStatus;

public record EmotionAnalysisResponse (HttpStatus status, EmotionType emotion) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import org.springframework.http.HttpStatus;

import java.util.List;

public record FaceDetectionResponse(
int statusCode,
String statusMessage,
String message
List<BoundingBox> faceBox
) {
public FaceDetectionResponse(HttpStatus httpStatus, String message) {
this(httpStatus.value(), httpStatus.name(), message);
public FaceDetectionResponse(HttpStatus httpStatus, List<BoundingBox> faceBox) {
this(httpStatus.value(), httpStatus.name(), faceBox);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bamboo.log.emotion.dto.req;

import com.bamboo.log.emotion.dto.BoundingBox;
import org.springframework.web.multipart.MultipartFile;

public record EmotionAnalysisRequest(MultipartFile image, BoundingBox faceBox) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.bamboo.log.emotion.service;

import com.bamboo.log.emotion.dto.EmotionAnalysisResponse;
import com.bamboo.log.emotion.dto.req.EmotionAnalysisRequest;

public interface EmotionAnalysisService {
EmotionAnalysisResponse analyzeEmotion(EmotionAnalysisRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.bamboo.log.emotion.service.impl;

import com.bamboo.log.emotion.domain.EmotionType;
import com.bamboo.log.emotion.dto.BoundingBox;
import com.bamboo.log.emotion.dto.EmotionAnalysisResponse;
import com.bamboo.log.emotion.dto.req.EmotionAnalysisRequest;
import com.bamboo.log.emotion.service.EmotionAnalysisService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Slf4j
@Service
@RequiredArgsConstructor
public class EmotionAnalysisServiceImpl implements EmotionAnalysisService {
@Value("${emotion.api.url}")
private String emotionApiUrl;

private final OkHttpClient client = new OkHttpClient();

@Override
public EmotionAnalysisResponse analyzeEmotion(EmotionAnalysisRequest request) {
MultipartFile image = request.image();
BoundingBox faceBox = request.faceBox();

log.info("Received EmotionAnalysisRequest: {}", request);
log.info("Received BoundingBox: {}", (faceBox != null) ? faceBox.toString() : "NULL");

if (faceBox == null) {
log.warn("얼굴 영역이 감지되지 않음. 감정 분석 불가능");
return new EmotionAnalysisResponse(HttpStatus.OK, EmotionType.NONE);
}

EmotionAnalysisResponse response = callFastAPI(image, request);

log.info("Received response: {}", response);
return response;
}

private EmotionAnalysisResponse callFastAPI(MultipartFile image, EmotionAnalysisRequest request) {
try {
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("image", image.getOriginalFilename(),
RequestBody.create(image.getBytes(), MediaType.parse(image.getContentType())))
.addFormDataPart("x1", String.valueOf(request.faceBox().x1()))
.addFormDataPart("y1", String.valueOf(request.faceBox().y1()))
.addFormDataPart("x2", String.valueOf(request.faceBox().x2()))
.addFormDataPart("y2", String.valueOf(request.faceBox().y2()))
.build();

Request fastApiRequest = new Request.Builder()
.url(emotionApiUrl)
.post(requestBody)
.addHeader("accept", "application/json")
.build();

try (Response response = client.newCall(fastApiRequest).execute()) {
if (!response.isSuccessful()) {
return new EmotionAnalysisResponse(HttpStatus.valueOf(response.code()), EmotionType.NONE);
}

String responseBody = response.body().string();
JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
String emotionString = jsonNode.get(0).get("emotion").asText();
EmotionType emotionType = EmotionType.valueOf(emotionString.toUpperCase());

log.info("Received response about api: {}", response);
return new EmotionAnalysisResponse(HttpStatus.OK, emotionType);
}
} catch (IOException e) {
log.error("FASTAPI 호출 중 오류 발생", e);
return new EmotionAnalysisResponse(HttpStatus.INTERNAL_SERVER_ERROR, EmotionType.NONE);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bamboo.log.emotion.service.impl;

import com.bamboo.log.emotion.dto.BoundingBox;
import com.bamboo.log.emotion.dto.FaceDetectionResponse;
import com.bamboo.log.emotion.service.FaceDetectionService;
import com.fasterxml.jackson.databind.JsonNode;
Expand All @@ -13,6 +14,8 @@
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
Expand All @@ -31,7 +34,7 @@ public class FaceDetectionServiceImpl implements FaceDetectionService {
public FaceDetectionResponse detectFace(MultipartFile image) {
if (image == null || image.isEmpty()) {
log.error("파일이 전달되지 않음.");
return new FaceDetectionResponse(HttpStatus.BAD_REQUEST, "파일이 전달되지 않았습니다.");
return new FaceDetectionResponse(HttpStatus.BAD_REQUEST, List.of());
}
log.info("파일 이름: {}", image.getOriginalFilename());
log.info("파일 크기: {} bytes", image.getSize());
Expand Down Expand Up @@ -61,7 +64,7 @@ public FaceDetectionResponse detectFace(MultipartFile image) {
log.info("API 응답 메시지: {}", response.message());

if (!response.isSuccessful()) {
return new FaceDetectionResponse(HttpStatus.valueOf(response.code()), "API 요청 실패: " + response.message());
return new FaceDetectionResponse(HttpStatus.valueOf(response.code()), List.of());
}

String responseBody = response.body().string();
Expand All @@ -72,18 +75,27 @@ public FaceDetectionResponse detectFace(MultipartFile image) {
JsonNode results = jsonNode.get("results");

// 얼굴 인식 여부 확인
if (results != null && results.isArray() && results.size() > 0) {
List<BoundingBox> boxList = new ArrayList<>();
if (results != null && results.isArray()) {
for (JsonNode result : results) {
if ("face".equals(result.get("name").asText())) {
return new FaceDetectionResponse(HttpStatus.OK, responseBody);
JsonNode boxNode = result.get("box");
if (boxNode != null) {
boxList.add(new BoundingBox(
boxNode.get("x1").asDouble(),
boxNode.get("y1").asDouble(),
boxNode.get("x2").asDouble(),
boxNode.get("y2").asDouble()
));
}
}
}
}
return new FaceDetectionResponse(HttpStatus.NOT_FOUND, "얼굴이 인식되지 않았습니다.");
return new FaceDetectionResponse(HttpStatus.OK, boxList);
}
} catch (IOException e) {
log.error("API 요청 중 예외 발생", e);
return new FaceDetectionResponse(HttpStatus.INTERNAL_SERVER_ERROR, "API 요청 중 오류 발생: " + e.getMessage());
return new FaceDetectionResponse(HttpStatus.INTERNAL_SERVER_ERROR, List.of());
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ elice:
url:
face: ${FACE_URL}
img: ${IMG_URL}
chat: ${CHAT_URL}
chat: ${CHAT_URL}
emotion:
api:
url: ${EMOTION_URL}