Skip to content

Commit

Permalink
Merge pull request #24 from urdego/feat(#23)
Browse files Browse the repository at this point in the history
Feat(#23): 컨텐츠 유해성 감지
  • Loading branch information
j-ra1n authored Jan 24, 2025
2 parents b2f6f02 + f2ead67 commit 3010a63
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 13 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ dependencies {

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

// Tensorflow (컨텐츠 유해성 감지)
implementation 'org.tensorflow:tensorflow-core-api:1.0.0-rc.2'
runtimeOnly 'org.tensorflow:tensorflow-core-native:1.0.0-rc.2:macosx-arm64'
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ public enum ExceptionMessage {
// Content save
INVALID_FILE_FORMAT("잘못된 형식의 컨텐츠입니다."),
CONTENT_SAVE_FAILED("컨텐츠 저장에 실패했습니다."),
CONTENT_MULTI_SAVE_FAILED("컨텐츠 다중 저장에 실패했습니다."),
CONTENT_MULTI_SAVE_FAILED("컨텐츠 다중 저장에 실패했습니다. (컨텐츠 유해성 감지)"),
CONTENT_DELETE_FAILED("컨텐츠 삭제에 실패했습니다."),
DIRECTORY_CREATION_FAILED("상위 폴더 생성 중 예외가 발생했습니다."),
CONTENT_NOT_ALLOWED("컨텐츠에 유해성이 감지되어 저장에 실패했습니다."),
CONTENT_ANALYSIS_FAILED("컨텐츠 분석에 실패했습니다."),

// Image
IMAGE_METADATA_FAILED("이미지 메타데이터 추출에 실패했습니다.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.urdego.urdego_content_service.common.exception.ExceptionMessage;
import io.urdego.urdego_content_service.common.exception.content.UserContentException;
import io.urdego.urdego_content_service.domain.service.dto.FileInfo;
import io.urdego.urdego_content_service.domain.service.model.nsfw.NSFWDetector;
import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile;

Expand All @@ -21,24 +22,32 @@ public class ContentCommander {

// 컨텐츠 저장
public static FileInfo saveContent(Long userId, MultipartFile content) {
String filename = createFilename(userId, content.getOriginalFilename());

Path savedContentPath = BASE_PATH.resolve(Path.of(String.valueOf(userId), filename));
createParentDirectories(savedContentPath);
try {
// NSFW 검사
if (NSFWDetector.isNSFW(content.getBytes())) {
throw new UserContentException(ExceptionMessage.CONTENT_NOT_ALLOWED);
}

try (InputStream is = content.getInputStream();
OutputStream os = Files.newOutputStream(savedContentPath)) {
String filename = createFilename(userId, content.getOriginalFilename());
Path savedContentPath = BASE_PATH.resolve(Path.of(String.valueOf(userId), filename));
createParentDirectories(savedContentPath);

try (InputStream is = content.getInputStream();
OutputStream os = Files.newOutputStream(savedContentPath)) {

// 컨텐츠 저장
StreamUtils.copy(is, os);
}

return FileInfo.builder()
.fileName(filename)
.savedPath(BASE_URL + "/" + userId + "/" + filename)
.build();

// 컨텐츠 저장
StreamUtils.copy(is, os);
} catch (IOException e) {
throw new UserContentException(ExceptionMessage.CONTENT_SAVE_FAILED);
}

return FileInfo.builder()
.fileName(filename)
.savedPath(BASE_URL + "/" + userId + "/" + filename)
.build();
}

// 컨텐츠 삭제
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.urdego.urdego_content_service.domain.service.model.nsfw;

import org.tensorflow.Graph;
import org.tensorflow.Session;
import org.tensorflow.Tensor;
import org.tensorflow.proto.GraphDef;
import org.tensorflow.types.TFloat32;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

// 유해 컨텐츠 감지 (NSFW = Not Safe For Work)
public class NSFWDetector {

private static final Path MODEL_PATH = Paths.get("src/main/resources/nsfw.pb");
private static final String INPUT_TENSOR = "input_1";
private static final String OUTPUT_TENSOR = "dense_3/Softmax";
private static final double NSFW_RATIO = 0.6;
private static final Graph graph;

static {
try {
graph = new Graph();
byte[] graphDef = Files.readAllBytes(MODEL_PATH);
graph.importGraphDef(GraphDef.parseFrom(graphDef));
} catch (IOException e) {
throw new RuntimeException("Failed to load GraphDef", e);
}
}


public static boolean isNSFW(byte[] imageBytes) throws IOException {
try (Session session = new Session(graph);
Tensor inputTensor = preprocessImage(imageBytes)) {

try (Tensor outputTensor = session.runner()
.feed(INPUT_TENSOR, inputTensor) // 입력 텐서 이름
.fetch(OUTPUT_TENSOR) // 출력 텐서 이름
.run()
.get(0)) {


try (var rawTensor = outputTensor.asRawTensor()) {
// float 데이터 버퍼 가져오기
var floatBuffer = rawTensor.data().asFloats();
int outputSize = (int) outputTensor.shape().asArray()[1]; // 출력 클래스 개수 확인
float[] probabilities = new float[outputSize];

// float 배열로 복사
floatBuffer.read(probabilities);

// 인덱스 3과 4의 값 확인
float nsfwProbabilityClass3 = probabilities[3]; // NSFW 3 (Explicit NSFW Content)
float nsfwProbabilityClass4 = probabilities[4]; // NSFW 4 (Porno NSFW Content)

// NSFW 여부 판단
return nsfwProbabilityClass3 > NSFW_RATIO || nsfwProbabilityClass4 > NSFW_RATIO;
}
}
}
}

private static TFloat32 preprocessImage(byte[] imageBytes) throws IOException {
BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageBytes));

// 224x224로 리사이즈
BufferedImage resized = new BufferedImage(224, 224, BufferedImage.TYPE_INT_RGB);
Graphics2D g = resized.createGraphics();
g.drawImage(img, 0, 0, 224, 224, null);
g.dispose();

// [0, 1] 범위로 정규화된 float 배열 생성
float[][][][] inputData = new float[1][224][224][3];
for (int y = 0; y < 224; y++) {
for (int x = 0; x < 224; x++) {
int rgb = resized.getRGB(x, y);
inputData[0][y][x][0] = ((rgb >> 16) & 0xFF) / 255.0f; // Red
inputData[0][y][x][1] = ((rgb >> 8) & 0xFF) / 255.0f; // Green
inputData[0][y][x][2] = (rgb & 0xFF) / 255.0f; // Blue
}
}

// Tensor 생성
return TFloat32.tensorOf(org.tensorflow.ndarray.StdArrays.ndCopyOf(inputData));
}

}
Binary file added src/main/resources/nsfw.pb
Binary file not shown.

0 comments on commit 3010a63

Please sign in to comment.