170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
const loadScript = (src: string, fallbackSrcs?: string[]): Promise<void> => {
|
|
return new Promise((resolve, reject) => {
|
|
const allSrcs = [src, ...(fallbackSrcs || [])];
|
|
|
|
// Check if any of the scripts are already loaded
|
|
if (allSrcs.some(s => document.querySelector(`script[src="${s}"]`))) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const tryLoadScript = (index: number) => {
|
|
if (index >= allSrcs.length) {
|
|
reject(new Error(`Failed to load script from any source: ${allSrcs.join(', ')}`));
|
|
return;
|
|
}
|
|
|
|
const currentSrc = allSrcs[index];
|
|
const script = document.createElement('script');
|
|
script.src = currentSrc;
|
|
|
|
script.onload = () => resolve();
|
|
script.onerror = () => {
|
|
console.warn(`Failed to load script ${currentSrc}. Trying next fallback...`);
|
|
const nextIndex = index + 1;
|
|
if (nextIndex < allSrcs.length) {
|
|
tryLoadScript(nextIndex);
|
|
} else {
|
|
reject(new Error(`Failed to load script from all sources: ${allSrcs.join(', ')}`));
|
|
}
|
|
};
|
|
|
|
document.head.appendChild(script);
|
|
};
|
|
|
|
tryLoadScript(0);
|
|
});
|
|
};
|
|
|
|
const loadModerationLibraries = async () => {
|
|
// Load TensorFlow first with fallbacks
|
|
await loadScript(
|
|
'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs',
|
|
['https://unpkg.com/@tensorflow/tfjs', 'https://esm.sh/@tensorflow/tfjs']
|
|
);
|
|
|
|
// Load models after tfjs is available, with multiple fallbacks
|
|
await Promise.all([
|
|
loadScript(
|
|
'https://cdn.jsdelivr.net/npm/@tensorflow-models/blazeface',
|
|
['https://unpkg.com/@tensorflow-models/blazeface', 'https://esm.sh/@tensorflow-models/blazeface']
|
|
),
|
|
// NSFWJS with 3 CDN fallbacks
|
|
loadScript(
|
|
'https://cdn.jsdelivr.net/npm/nsfwjs@2.4.0/dist/bundle.js',
|
|
[
|
|
'https://unpkg.com/nsfwjs@2.4.0/dist/bundle.js',
|
|
'https://esm.sh/nsfwjs@2.4.0/dist/bundle.js'
|
|
]
|
|
)
|
|
]);
|
|
};
|
|
|
|
export const processImageModeration = async (file: File): Promise<{ file: File; blocked: boolean }> => {
|
|
try {
|
|
const settingsRes = await fetch('/api/v1/moderation/settings');
|
|
if (!settingsRes.ok) return { file, blocked: false };
|
|
const settings = await settingsRes.json();
|
|
const { blockNsfw, blurFaces } = settings;
|
|
|
|
if (!blockNsfw && !blurFaces) {
|
|
return { file, blocked: false };
|
|
}
|
|
|
|
// Try to load moderation libraries, but don't fail if they're unavailable
|
|
try {
|
|
await loadModerationLibraries();
|
|
} catch (libLoadErr) {
|
|
console.warn('Moderation libraries failed to load, proceeding without NSFW/Face blur checks:', libLoadErr);
|
|
return { file, blocked: false };
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const img = new Image();
|
|
img.onload = async () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) {
|
|
resolve({ file, blocked: false });
|
|
return;
|
|
}
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
if (blockNsfw) {
|
|
try {
|
|
const nsfwModel = await (window as any).nsfwjs?.load();
|
|
if (nsfwModel) {
|
|
const predictions = await nsfwModel.classify(canvas);
|
|
const pornOrHentai = predictions.find((p: any) => p.className === 'Porn' || p.className === 'Hentai');
|
|
const pornProb = pornOrHentai ? pornOrHentai.probability : 0;
|
|
if (pornProb > 0.5) {
|
|
console.warn(`Image blocked by NSFW filter (probability: ${pornProb})`);
|
|
resolve({ file, blocked: true });
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('NSFW validation error (will allow upload):', e);
|
|
}
|
|
}
|
|
|
|
let modified = false;
|
|
if (blurFaces) {
|
|
try {
|
|
const blazefaceModel = await (window as any).blazeface?.load();
|
|
if (blazefaceModel) {
|
|
const predictions = await blazefaceModel.estimateFaces(canvas, false);
|
|
if (predictions && predictions.length > 0) {
|
|
modified = true;
|
|
predictions.forEach((prediction: any) => {
|
|
const startX = prediction.topLeft[0];
|
|
const startY = prediction.topLeft[1];
|
|
const endX = prediction.bottomRight[0];
|
|
const endY = prediction.bottomRight[1];
|
|
const width = endX - startX;
|
|
const height = endY - startY;
|
|
|
|
const faceCanvas = document.createElement('canvas');
|
|
faceCanvas.width = width;
|
|
faceCanvas.height = height;
|
|
const faceCtx = faceCanvas.getContext('2d');
|
|
if (faceCtx) {
|
|
faceCtx.drawImage(canvas, startX, startY, width, height, 0, 0, width, height);
|
|
ctx.filter = 'blur(15px)';
|
|
ctx.drawImage(faceCanvas, startX, startY, width, height);
|
|
ctx.filter = 'none';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Face blur error (will skip face detection):', e);
|
|
}
|
|
}
|
|
|
|
if (modified) {
|
|
canvas.toBlob((blob) => {
|
|
if (blob) {
|
|
const processedFile = new File([blob], file.name, { type: file.type });
|
|
resolve({ file: processedFile, blocked: false });
|
|
} else {
|
|
resolve({ file, blocked: false });
|
|
}
|
|
}, file.type);
|
|
} else {
|
|
resolve({ file, blocked: false });
|
|
}
|
|
};
|
|
img.onerror = () => {
|
|
resolve({ file, blocked: false });
|
|
};
|
|
img.src = URL.createObjectURL(file);
|
|
});
|
|
} catch (err) {
|
|
console.error('Image moderation process failed:', err);
|
|
return { file, blocked: false };
|
|
}
|
|
};
|