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 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 };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue