feat: automatic image moderation
This commit is contained in:
parent
b6315ee76c
commit
50c18a342d
1 changed files with 32 additions and 37 deletions
|
|
@ -1,40 +1,29 @@
|
|||
// import * as tf from "@tensorflow/tfjs-node";
|
||||
// import * as nsfwjs from "nsfwjs";
|
||||
import sharp from "sharp";
|
||||
import { fileTypeFromBuffer } from "file-type";
|
||||
|
||||
const MIN_IMAGE_DIMENSIONS = 128;
|
||||
const MAX_IMAGE_DIMENSIONS = 1024;
|
||||
const MAX_IMAGE_SIZE = 1024 * 1024; // 1 MB
|
||||
|
||||
// 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!;
|
||||
// }
|
||||
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||
|
||||
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.size > MAX_IMAGE_SIZE)
|
||||
return { valid: false, error: `One or more of your images are too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` };
|
||||
if (file.size > MAX_IMAGE_SIZE) return { valid: false, error: `Image too large. Maximum size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB` };
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// Check mime type
|
||||
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
|
||||
if (
|
||||
|
|
@ -49,25 +38,31 @@ export async function validateImage(file: File): Promise<{ valid: boolean; error
|
|||
}
|
||||
|
||||
// Check for inappropriate content
|
||||
// const image = tf.node.decodeImage(buffer, 3) as tf.Tensor3D;
|
||||
// const model = await loadModel();
|
||||
// const predictions = await model.classify(image);
|
||||
// image.dispose();
|
||||
// https://github.com/trafficlunar/api-moderation
|
||||
try {
|
||||
const blob = new Blob([buffer]);
|
||||
const formData = new FormData();
|
||||
formData.append("image", blob);
|
||||
|
||||
// for (const pred of predictions) {
|
||||
// if (
|
||||
// (pred.className === "Porn" && pred.probability > THRESHOLD) ||
|
||||
// (pred.className === "Hentai" && pred.probability > THRESHOLD) ||
|
||||
// (pred.className === "Sexy" && pred.probability > THRESHOLD)
|
||||
// ) {
|
||||
// // reject image
|
||||
// return { valid: false, error: "Image contains inappropriate content" };
|
||||
// }
|
||||
// }
|
||||
const moderationResponse = await fetch("https://api.trafficlunar.net/moderate/image", { method: "POST", body: formData });
|
||||
const result = await moderationResponse.json();
|
||||
|
||||
if (!moderationResponse.ok) {
|
||||
console.error("Moderation API error:", result);
|
||||
return { valid: false, error: "Content moderation check failed", status: 500 };
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error("Error validating image:", error);
|
||||
return { valid: false, error: "Failed to process image file.", status: 500 };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue