feat: automatic image moderation

This commit is contained in:
trafficlunar 2025-04-28 16:42:57 +01:00
parent b6315ee76c
commit 50c18a342d

View file

@ -1,40 +1,29 @@
// import * as tf from "@tensorflow/tfjs-node";
// import * as nsfwjs from "nsfwjs";
import sharp from "sharp"; import sharp from "sharp";
import { fileTypeFromBuffer } from "file-type"; import { fileTypeFromBuffer } from "file-type";
const MIN_IMAGE_DIMENSIONS = 128; const MIN_IMAGE_DIMENSIONS = 128;
const MAX_IMAGE_DIMENSIONS = 1024; const MAX_IMAGE_DIMENSIONS = 1024;
const MAX_IMAGE_SIZE = 1024 * 1024; // 1 MB const MAX_IMAGE_SIZE = 1024 * 1024; // 1 MB
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
// const THRESHOLD = 0.5;
// tf.enableProdMode();
// Load NSFW.JS model
// let _model: nsfwjs.NSFWJS | undefined = undefined;
// async function loadModel() {
// if (!_model) {
// const model = await nsfwjs.load("MobileNetV2Mid");
// _model = model;
// }
// return _model!;
// }
export async function validateImage(file: File): Promise<{ valid: boolean; error?: string; status?: number }> { export async function validateImage(file: File): Promise<{ valid: boolean; error?: string; status?: number }> {
if (!file || file.size == 0) return { valid: false, error: "Empty image file" }; if (!file || file.size == 0) return { valid: false, error: "Empty image file" };
if (file.size > MAX_IMAGE_SIZE) if (file.size > MAX_IMAGE_SIZE) return { valid: false, error: `Image too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` };
return { valid: false, error: `One or more of your images are too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` };
try { try {
const buffer = Buffer.from(await file.arrayBuffer()); const buffer = Buffer.from(await file.arrayBuffer());
// Check mime type // Check mime type
const fileType = await fileTypeFromBuffer(buffer); const fileType = await fileTypeFromBuffer(buffer);
if (!fileType || !fileType.mime.startsWith("image/")) return { valid: false, error: "Invalid image file type. Only actual images are allowed" }; if (!fileType || !ALLOWED_MIME_TYPES.includes(fileType.mime))
return { valid: false, error: "Invalid image file type. Only .jpeg, .png, .gif, and .webp are allowed" };
const metadata = await sharp(buffer).metadata(); let metadata: sharp.Metadata;
try {
metadata = await sharp(buffer).metadata();
} catch {
return { valid: false, error: "Invalid or corrupted image file" };
}
// Check image dimensions // Check image dimensions
if ( if (
@ -49,25 +38,31 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error
} }
// Check for inappropriate content // Check for inappropriate content
// const image = tf.node.decodeImage(buffer, 3) as tf.Tensor3D; // https://github.com/trafficlunar/api-moderation
// const model = await loadModel(); try {
// const predictions = await model.classify(image); const blob = new Blob([buffer]);
// image.dispose(); const formData = new FormData();
formData.append("image", blob);
// for (const pred of predictions) { const moderationResponse = await fetch("https://api.trafficlunar.net/moderate/image", { method: "POST", body: formData });
// if ( const result = await moderationResponse.json();
// (pred.className === "Porn" && pred.probability > THRESHOLD) ||
// (pred.className === "Hentai" && pred.probability > THRESHOLD) || if (!moderationResponse.ok) {
// (pred.className === "Sexy" && pred.probability > THRESHOLD) console.error("Moderation API error:", result);
// ) { return { valid: false, error: "Content moderation check failed", status: 500 };
// // reject image }
// return { valid: false, error: "Image contains inappropriate content" };
// } if (result.error) {
// } return { valid: false, error: result.error };
}
} catch (moderationError) {
console.error("Error fetching moderation API:", moderationError);
return { valid: false, error: "Moderation API is down", status: 503 };
}
return { valid: true };
} catch (error) { } catch (error) {
console.error("Error validating image:", error); console.error("Error validating image:", error);
return { valid: false, error: "Failed to process image file.", status: 500 }; return { valid: false, error: "Failed to process image file.", status: 500 };
} }
return { valid: true };
} }