[backend] Remove nsfw detection / tensorflow

This commit is contained in:
Laura Hausmann 2023-10-18 23:09:35 +02:00
parent 6b45b7019c
commit 5c7a663320
No known key found for this signature in database
GPG Key ID: D044E84C5BE01605
93 changed files with 73 additions and 2544 deletions

1032
.pnp.cjs generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57a45e8e821a9e418778fc4daf33a2dd2e38d7b8830ccb2918fa72875d24337e
size 4133297

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbace95fc23a53271063949c1ca035fb1c51f81667f3626a41aa2c96eb22ba13
size 769948

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2fb2bc4c7d994ecd579dd1bd18cb6892b35a213ac618ddc8ddaddf81b5aad12b
size 24318

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1fd0be5e883277b6a29336f593b3b2d9ed91b6e55e97242440047f979c4483b6
size 88191

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e0567b915b7cd142d8f2106a80bbcd3da42ee6bbc44980f20604c28e159e29b8
size 19137

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/isarray-npm-0.0.1-92e37e0a70-49191f1425.zip (Stored with Git LFS) vendored

Binary file not shown.

BIN
.yarn/cache/jpeg-js-npm-0.3.7-fe3ece5658-85a1ab09fe.zip (Stored with Git LFS) vendored

Binary file not shown.

BIN
.yarn/cache/long-npm-4.0.0-ecd96a31ed-16afbe8f74.zip (Stored with Git LFS) vendored

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd89305124e1950f8e1f7597778aaa66079733fabf77437b53d5ee6523f5c52a
size 6732

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c71b8d36d42cd2718c94fd7a46b597055ae37ad49bca92dcb6be20c1e2039e6
size 469978

Binary file not shown.

BIN
.yarn/cache/omggif-npm-1.0.10-6ffa5bf343-15102e46b6.zip (Stored with Git LFS) vendored

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df9df02c19f8be5b66e5e2e4b72aabb5516b2670e9bc0f9b58cc88966491044b
size 3495

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/qs-npm-6.5.3-90b2635484-6f20bf08ca.zip (Stored with Git LFS) vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/rimraf-npm-2.7.1-9a71f3cc37-cdc7f6eacb.zip (Stored with Git LFS) vendored

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/tar-npm-4.4.19-5f8e81b87e-423c8259b1.zip (Stored with Git LFS) vendored

Binary file not shown.

BIN
.yarn/cache/through-npm-2.3.4-eb93babff1-bab392560a.zip (Stored with Git LFS) vendored

Binary file not shown.

BIN
.yarn/cache/uuid-npm-3.4.0-4fd8ef88ad-58de2feed6.zip (Stored with Git LFS) vendored

Binary file not shown.

View File

@ -44,7 +44,6 @@
"dependencies": {
"@bull-board/api": "5.6.0",
"@bull-board/ui": "5.6.0",
"@tensorflow/tfjs": "^3.21.0",
"js-yaml": "4.1.0",
"seedrandom": "^3.0.5"
},

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:994515b0f73407d71740301e9877511b086609dbdc7ead1b31bcb82b3397ae74
size 4194304

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@tensorflow/tfjs-node": "3.21.1",
"@types/formidable": "^2.0.5"
},
"dependencies": {
@ -36,9 +35,6 @@
"@redocly/openapi-core": "1.0.0-beta.131",
"@sinonjs/fake-timers": "9.1.2",
"@syuilo/aiscript": "0.11.1",
"@tensorflow/tfjs": "^4.2.0",
"@tensorflow/tfjs-backend-webgl": "^4.2.0",
"@tensorflow/tfjs-core": "^4.2.0",
"adm-zip": "^0.5.10",
"ajv": "8.12.0",
"archiver": "5.3.1",
@ -98,7 +94,6 @@
"nested-property": "4.0.0",
"node-fetch": "3.3.1",
"nodemailer": "6.9.3",
"nsfwjs": "2.4.2",
"oauth": "^0.10.0",
"os-utils": "0.0.14",
"otpauth": "^9.1.3",

View File

@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class RemoteNsfwDetection1697663824168 implements MigrationInterface {
name = 'RemoteNsfwDetection1697663824168'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_3b33dff77bb64b23c88151d23e"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8bdcd3dd2bddb78014999a16ce"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "maybeSensitive"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "maybePorn"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetection"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitivemediadetection_enum"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionSensitivity"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitivemediadetectionsensitivity_enum"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "setSensitiveFlagAutomatically"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableSensitiveMediaDetectionForVideos"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoSensitive"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableSensitiveMediaDetectionForVideos" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "setSensitiveFlagAutomatically" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitivemediadetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionSensitivity" "public"."meta_sensitivemediadetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitivemediadetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetection" "public"."meta_sensitivemediadetection_enum" NOT NULL DEFAULT 'none'`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "maybePorn" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "maybeSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_8bdcd3dd2bddb78014999a16ce" ON "drive_file" ("maybePorn") `);
await queryRunner.query(`CREATE INDEX "IDX_3b33dff77bb64b23c88151d23e" ON "drive_file" ("maybeSensitive") `);
}
}

View File

@ -1,18 +1,12 @@
import * as fs from "node:fs";
import * as crypto from "node:crypto";
import { join } from "node:path";
import * as stream from "node:stream";
import * as util from "node:util";
import { FSWatcher } from "chokidar";
import { fileTypeFromFile } from "file-type";
import probeImageSize from "probe-image-size";
import FFmpeg from "fluent-ffmpeg";
import isSvg from "is-svg";
import { type predictionType } from "nsfwjs";
import sharp from "sharp";
import { encode } from "blurhash";
import { detectSensitive } from "@/services/detect-sensitive.js";
import { createTempDir } from "./create-temp.js";
const pipeline = util.promisify(stream.pipeline);
@ -27,8 +21,6 @@ export type FileInfo = {
height?: number;
orientation?: number;
blurhash?: string;
sensitive: boolean;
porn: boolean;
warnings: string[];
};
@ -47,12 +39,6 @@ const TYPE_SVG = {
*/
export async function getFileInfo(
path: string,
opts: {
skipSensitiveDetection: boolean;
sensitiveThreshold?: number;
sensitiveThresholdForPorn?: number;
enableSensitiveMediaDetectionForVideos?: boolean;
},
): Promise<FileInfo> {
const warnings = [] as string[];
@ -123,26 +109,6 @@ export async function getFileInfo(
});
}
let sensitive = false;
let porn = false;
if (!opts.skipSensitiveDetection) {
await detectSensitivity(
path,
type.mime,
opts.sensitiveThreshold ?? 0.5,
opts.sensitiveThresholdForPorn ?? 0.75,
opts.enableSensitiveMediaDetectionForVideos ?? false,
).then(
(value) => {
[sensitive, porn] = value;
},
(error) => {
warnings.push(`detectSensitivity failed: ${error}`);
},
);
}
return {
size,
md5,
@ -151,183 +117,10 @@ export async function getFileInfo(
height,
orientation,
blurhash,
sensitive,
porn,
warnings,
};
}
async function detectSensitivity(
source: string,
mime: string,
sensitiveThreshold: number,
sensitiveThresholdForPorn: number,
analyzeVideo: boolean,
): Promise<[sensitive: boolean, porn: boolean]> {
let sensitive = false;
let porn = false;
function judgePrediction(
result: readonly predictionType[],
): [sensitive: boolean, porn: boolean] {
let sensitive = false;
let porn = false;
if (
(result.find((x) => x.className === "Sexy")?.probability ?? 0) >
sensitiveThreshold
)
sensitive = true;
if (
(result.find((x) => x.className === "Hentai")?.probability ?? 0) >
sensitiveThreshold
)
sensitive = true;
if (
(result.find((x) => x.className === "Porn")?.probability ?? 0) >
sensitiveThreshold
)
sensitive = true;
if (
(result.find((x) => x.className === "Porn")?.probability ?? 0) >
sensitiveThresholdForPorn
)
porn = true;
return [sensitive, porn];
}
if (["image/jpeg", "image/png", "image/webp"].includes(mime)) {
const result = await detectSensitive(source);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
} else if (
analyzeVideo &&
(mime === "image/apng" || mime.startsWith("video/"))
) {
const [outDir, disposeOutDir] = await createTempDir();
try {
const command = FFmpeg()
.input(source)
.inputOptions([
"-skip_frame",
"nokey", // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない)
"-lowres",
"3", // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない)
])
.noAudio()
.videoFilters([
{
filter: "select", // フレームのフィルタリング
options: {
e: "eq(pict_type,PICT_TYPE_I)", // I-Frame のみをフィルタするVP9 とかはデコードしてみないとわからないっぽい)
},
},
{
filter: "blackframe", // 暗いフレームの検出
options: {
amount: "0", // 暗さに関わらず全てのフレームで測定値を取る
},
},
{
filter: "metadata",
options: {
mode: "select", // フレーム選択モード
key: "lavfi.blackframe.pblack", // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する)
value: "50",
function: "less", // 50% 未満のフレームを選択する50% 以上暗部があるフレームだと誤検知を招くかもしれないので)
},
},
{
filter: "scale",
options: {
w: 299,
h: 299,
},
},
])
.format("image2")
.output(join(outDir, "%d.png"))
.outputOptions(["-vsync", "0"]); // 可変フレームレートにすることで穴埋めをさせない
const results: ReturnType<typeof judgePrediction>[] = [];
let frameIndex = 0;
let targetIndex = 0;
let nextIndex = 1;
for await (const path of asyncIterateFrames(outDir, command)) {
try {
const index = frameIndex++;
if (index !== targetIndex) {
continue;
}
targetIndex = nextIndex;
nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける
const result = await detectSensitive(path);
if (result) {
results.push(judgePrediction(result));
}
} finally {
fs.promises.unlink(path);
}
}
sensitive =
results.filter((x) => x[0]).length >=
Math.ceil(results.length * sensitiveThreshold);
porn =
results.filter((x) => x[1]).length >=
Math.ceil(results.length * sensitiveThresholdForPorn);
} finally {
disposeOutDir();
}
}
return [sensitive, porn];
}
async function* asyncIterateFrames(
cwd: string,
command: FFmpeg.FfmpegCommand,
): AsyncGenerator<string, void> {
const watcher = new FSWatcher({
cwd,
disableGlobbing: true,
});
let finished = false;
command.once("end", () => {
finished = true;
watcher.close();
});
command.run();
for (let i = 1; true; i++) {
const current = `${i}.png`;
const next = `${i + 1}.png`;
const framePath = join(cwd, current);
if (await exists(join(cwd, next))) {
yield framePath;
} else if (!finished) {
watcher.add(next);
await new Promise<void>((resolve, reject) => {
watcher.on("add", function onAdd(path) {
if (path === next) {
// 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている
watcher.unwatch(current);
watcher.off("add", onAdd);
resolve();
}
});
command.once("end", resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている
command.once("error", reject);
});
yield framePath;
} else if (await exists(framePath)) {
yield framePath;
} else {
return;
}
}
}
function exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(
() => true,

View File

@ -183,19 +183,6 @@ export class DriveFile {
})
public isSensitive: boolean;
@Index()
@Column("boolean", {
default: false,
comment: "Whether the DriveFile is NSFW. (predict)",
})
public maybeSensitive: boolean;
@Index()
@Column("boolean", {
default: false,
})
public maybePorn: boolean;
/**
* ()URLへの直リンクか否か
*/

View File

@ -253,33 +253,6 @@ export class Meta {
})
public recaptchaSecretKey: string | null;
@Column("enum", {
enum: ["none", "all", "local", "remote"],
default: "none",
})
public sensitiveMediaDetection: "none" | "all" | "local" | "remote";
@Column("enum", {
enum: ["medium", "low", "high", "veryLow", "veryHigh"],
default: "medium",
})
public sensitiveMediaDetectionSensitivity:
| "medium"
| "low"
| "high"
| "veryLow"
| "veryHigh";
@Column("boolean", {
default: false,
})
public setSensitiveFlagAutomatically: boolean;
@Column("boolean", {
default: false,
})
public enableSensitiveMediaDetectionForVideos: boolean;
@Column("integer", {
default: 1024,
comment: "Drive capacity of a local user (MB)",

View File

@ -182,11 +182,6 @@ export class UserProfile {
})
public alwaysMarkNsfw: boolean;
@Column("boolean", {
default: false,
})
public autoSensitive: boolean;
@Column("boolean", {
default: false,
})
@ -268,4 +263,4 @@ type IMentionedRemoteUsers = {
url?: string;
username: string;
host: string;
}[]
}[]

View File

@ -539,7 +539,6 @@ export const UserRepository = db.getRepository(User).extend({
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
autoSensitive: profile!.autoSensitive,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle,

View File

@ -374,11 +374,6 @@ export const packedMeDetailedOnlySchema = {
nullable: true,
optional: false,
},
autoSensitive: {
type: "boolean",
nullable: true,
optional: false,
},
carefulBot: {
type: "boolean",
nullable: true,

View File

@ -2,7 +2,6 @@ import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import define from "../../define.js";
import { Exp } from "@tensorflow/tfjs";
export const meta = {
tags: ["meta"],
@ -266,26 +265,6 @@ export const meta = {
optional: true,
nullable: true,
},
sensitiveMediaDetection: {
type: "string",
optional: true,
nullable: false,
},
sensitiveMediaDetectionSensitivity: {
type: "string",
optional: true,
nullable: false,
},
setSensitiveFlagAutomatically: {
type: "boolean",
optional: true,
nullable: false,
},
enableSensitiveMediaDetectionForVideos: {
type: "boolean",
optional: true,
nullable: false,
},
proxyAccountId: {
type: "string",
optional: true,
@ -531,12 +510,6 @@ export default define(meta, paramDef, async (ps, me) => {
secureMode: instance.secureMode,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
sensitiveMediaDetection: instance.sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity:
instance.sensitiveMediaDetectionSensitivity,
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos:
instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,

View File

@ -62,7 +62,6 @@ export default define(meta, paramDef, async (ps, me) => {
noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning,
alwaysMarkNsfw: profile.alwaysMarkNsfw,
autoSensitive: profile.autoSensitive,
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,

View File

@ -101,16 +101,6 @@ export const paramDef = {
enableRecaptcha: { type: "boolean" },
recaptchaSiteKey: { type: "string", nullable: true },
recaptchaSecretKey: { type: "string", nullable: true },
sensitiveMediaDetection: {
type: "string",
enum: ["none", "all", "local", "remote"],
},
sensitiveMediaDetectionSensitivity: {
type: "string",
enum: ["medium", "low", "high", "veryLow", "veryHigh"],
},
setSensitiveFlagAutomatically: { type: "boolean" },
enableSensitiveMediaDetectionForVideos: { type: "boolean" },
proxyAccountId: { type: "string", format: "misskey:id", nullable: true },
maintainerName: { type: "string", nullable: true },
maintainerEmail: { type: "string", nullable: true },
@ -148,8 +138,6 @@ export const paramDef = {
smtpPort: { type: "integer", nullable: true },
smtpUser: { type: "string", nullable: true },
smtpPass: { type: "string", nullable: true },
swPublicKey: { type: "string", nullable: true },
swPrivateKey: { type: "string", nullable: true },
tosUrl: { type: "string", nullable: true },
repositoryUrl: { type: "string" },
feedbackUrl: { type: "string" },
@ -348,24 +336,6 @@ export default define(meta, paramDef, async (ps, me) => {
set.recaptchaSecretKey = ps.recaptchaSecretKey;
}
if (ps.sensitiveMediaDetection !== undefined) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
}
if (ps.sensitiveMediaDetectionSensitivity !== undefined) {
set.sensitiveMediaDetectionSensitivity =
ps.sensitiveMediaDetectionSensitivity;
}
if (ps.setSensitiveFlagAutomatically !== undefined) {
set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically;
}
if (ps.enableSensitiveMediaDetectionForVideos !== undefined) {
set.enableSensitiveMediaDetectionForVideos =
ps.enableSensitiveMediaDetectionForVideos;
}
if (ps.proxyAccountId !== undefined) {
set.proxyAccountId = ps.proxyAccountId;
}
@ -462,14 +432,6 @@ export default define(meta, paramDef, async (ps, me) => {
set.errorImageUrl = ps.errorImageUrl;
}
if (ps.swPublicKey !== undefined) {
set.swPublicKey = ps.swPublicKey;
}
if (ps.swPrivateKey !== undefined) {
set.swPrivateKey = ps.swPrivateKey;
}
if (ps.tosUrl !== undefined) {
set.ToSUrl = ps.tosUrl;
}

View File

@ -123,7 +123,6 @@ export const paramDef = {
injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" },
alwaysMarkNsfw: { type: "boolean" },
autoSensitive: { type: "boolean" },
ffVisibility: { type: "string", enum: ["public", "followers", "private"] },
pinnedPageId: { type: "string", format: "misskey:id", nullable: true },
mutedWords: { type: "array" },
@ -213,8 +212,6 @@ export default define(meta, paramDef, async (ps, _user, token) => {
profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === "boolean")
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (typeof ps.autoSensitive === "boolean")
profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined)
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;

View File

@ -1,55 +0,0 @@
import * as fs from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import * as nsfw from "nsfwjs";
import si from "systeminformation";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const REQUIRED_CPU_FLAGS = ["avx2", "fma"];
let isSupportedCpu: undefined | boolean = undefined;
let model: nsfw.NSFWJS;
export async function detectSensitive(
path: string,
): Promise<nsfw.predictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
const cpuFlags = await getCpuFlags();
isSupportedCpu = REQUIRED_CPU_FLAGS.every((required) =>
cpuFlags.includes(required),
);
}
if (!isSupportedCpu) {
console.error("This CPU cannot use TensorFlow.");
return null;
}
const tf = await import("@tensorflow/tfjs-node");
if (model == null)
model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, {
size: 299,
});
const buffer = await fs.promises.readFile(path);
const image = (await tf.node.decodeImage(buffer, 3)) as any;
try {
const predictions = await model.classify(image);
return predictions;
} finally {
image.dispose();
}
} catch (err) {
console.error(err);
return null;
}
}
async function getCpuFlags(): Promise<string[]> {
const str = await si.cpuFlags();
return str.split(/\s+/);
}

View File

@ -396,7 +396,7 @@ async function expireOldFile(user: IRemoteUser, driveCapacity: number) {
for (const fileId of exceedFileIds) {
const file = await DriveFiles.findOneBy({ id: fileId });
deleteFile(file, true);
if (file) deleteFile(file, true);
}
}
@ -448,39 +448,7 @@ export async function addFile({
requestIp = null,
requestHeaders = null,
}: AddFileArgs): Promise<DriveFile> {
let skipNsfwCheck = false;
const instance = await fetchMeta();
if (user == null) skipNsfwCheck = true;
if (instance.sensitiveMediaDetection === "none") skipNsfwCheck = true;
if (
user &&
instance.sensitiveMediaDetection === "local" &&
Users.isRemoteUser(user)
)
skipNsfwCheck = true;
if (
user &&
instance.sensitiveMediaDetection === "remote" &&
Users.isLocalUser(user)
)
skipNsfwCheck = true;
const info = await getFileInfo(path, {
skipSensitiveDetection: skipNsfwCheck,
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
instance.sensitiveMediaDetectionSensitivity === "veryHigh"
? 0.1
: instance.sensitiveMediaDetectionSensitivity === "high"
? 0.3
: instance.sensitiveMediaDetectionSensitivity === "low"
? 0.7
: instance.sensitiveMediaDetectionSensitivity === "veryLow"
? 0.9
: 0.5,
sensitiveThresholdForPorn: 0.75,
enableSensitiveMediaDetectionForVideos:
instance.enableSensitiveMediaDetectionForVideos,
});
const info = await getFileInfo(path);
logger.info(`${JSON.stringify(info)}`);
// 現状 false positive が多すぎて実用に耐えない
@ -595,8 +563,6 @@ export async function addFile({
file.isLink = isLink;
file.requestIp = requestIp;
file.requestHeaders = requestHeaders;
file.maybeSensitive = info.sensitive;
file.maybePorn = info.porn;
file.isSensitive = user
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw
? true
@ -605,10 +571,6 @@ export async function addFile({
: false
: false;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (info.sensitive && instance.setSensitiveFlagAutomatically)
file.isSensitive = true;
if (url !== null) {
file.src = url;

View File

@ -29,116 +29,6 @@
<XBotProtection />
</FormFolder>
<FormFolder class="_formBlock">
<template #icon
><i class="ph-eye-slash ph-bold ph-lg"></i
></template>
<template #label>{{
i18n.ts.sensitiveMediaDetection
}}</template>
<template
v-if="sensitiveMediaDetection === 'all'"
#suffix
>{{ i18n.ts.all }}</template
>
<template
v-else-if="sensitiveMediaDetection === 'local'"
#suffix
>{{ i18n.ts.localOnly }}</template
>
<template
v-else-if="sensitiveMediaDetection === 'remote'"
#suffix
>{{ i18n.ts.remoteOnly }}</template
>
<template v-else #suffix>{{ i18n.ts.none }}</template>
<div class="_formRoot">
<span class="_formBlock">{{
i18n.ts._sensitiveMediaDetection.description
}}</span>
<FormRadios
v-model="sensitiveMediaDetection"
class="_formBlock"
>
<option value="none">{{ i18n.ts.none }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="local">
{{ i18n.ts.localOnly }}
</option>
<option value="remote">
{{ i18n.ts.remoteOnly }}
</option>
</FormRadios>
<FormRange
v-model="sensitiveMediaDetectionSensitivity"
:min="0"
:max="4"
:step="1"
:text-converter="(v) => `${v + 1}`"
class="_formBlock"
>
<template #label>{{
i18n.ts._sensitiveMediaDetection.sensitivity
}}</template>
<template #caption>{{
i18n.ts._sensitiveMediaDetection
.sensitivityDescription
}}</template>
</FormRange>
<FormSwitch
v-model="enableSensitiveMediaDetectionForVideos"
class="_formBlock"
>
<template #label
>{{
i18n.ts._sensitiveMediaDetection
.analyzeVideos
}}<span class="_beta">{{
i18n.ts.beta
}}</span></template
>
<template #caption>{{
i18n.ts._sensitiveMediaDetection
.analyzeVideosDescription
}}</template>
</FormSwitch>
<FormSwitch
v-model="setSensitiveFlagAutomatically"
class="_formBlock"
>
<template #label
>{{
i18n.ts._sensitiveMediaDetection
.setSensitiveFlagAutomatically
}}
({{ i18n.ts.notRecommended }})</template
>
<template #caption>{{
i18n.ts._sensitiveMediaDetection
.setSensitiveFlagAutomaticallyDescription
}}</template>
</FormSwitch>
<!-- 現状 false positive が多すぎて実用に耐えない
<FormSwitch v-model="disallowUploadWhenPredictedAsPorn" class="_formBlock">
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
</FormSwitch>
-->
<FormButton primary class="_formBlock" @click="save"
><i
class="ph-floppy-disk-back ph-bold ph-lg"
></i>
{{ i18n.ts.save }}</FormButton
>
</div>
</FormFolder>
<FormFolder class="_formBlock">
<template #label>Active Email Validation</template>
<template v-if="enableActiveEmailValidation" #suffix
@ -274,10 +164,6 @@ import { definePageMetadata } from "@/scripts/page-metadata";
let summalyProxy: string = $ref("");
let enableHcaptcha: boolean = $ref(false);
let enableRecaptcha: boolean = $ref(false);
let sensitiveMediaDetection: string = $ref("none");
let sensitiveMediaDetectionSensitivity: number = $ref(0);
let setSensitiveFlagAutomatically: boolean = $ref(false);
let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
let enableIpLogging: boolean = $ref(false);
let enableActiveEmailValidation: boolean = $ref(false);
@ -290,22 +176,6 @@ async function init() {
summalyProxy = meta.summalyProxy;
enableHcaptcha = meta.enableHcaptcha;
enableRecaptcha = meta.enableRecaptcha;
sensitiveMediaDetection = meta.sensitiveMediaDetection;
sensitiveMediaDetectionSensitivity =
meta.sensitiveMediaDetectionSensitivity === "veryLow"
? 0
: meta.sensitiveMediaDetectionSensitivity === "low"
? 1
: meta.sensitiveMediaDetectionSensitivity === "medium"
? 2
: meta.sensitiveMediaDetectionSensitivity === "high"
? 3
: meta.sensitiveMediaDetectionSensitivity === "veryHigh"
? 4
: 0;
setSensitiveFlagAutomatically = meta.setSensitiveFlagAutomatically;
enableSensitiveMediaDetectionForVideos =
meta.enableSensitiveMediaDetectionForVideos;
enableIpLogging = meta.enableIpLogging;
enableActiveEmailValidation = meta.enableActiveEmailValidation;
@ -317,21 +187,6 @@ async function init() {
function save() {
os.apiWithDialog("admin/update-meta", {
summalyProxy,
sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity:
sensitiveMediaDetectionSensitivity === 0
? "veryLow"
: sensitiveMediaDetectionSensitivity === 1
? "low"
: sensitiveMediaDetectionSensitivity === 2
? "medium"
: sensitiveMediaDetectionSensitivity === 3
? "high"
: sensitiveMediaDetectionSensitivity === 4
? "veryHigh"
: 0,
setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos,
enableIpLogging,
enableActiveEmailValidation,
}).then(() => {

View File

@ -54,19 +54,6 @@
>
<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
</FormSwitch>
<FormSwitch
v-model="autoSensitive"
class="_formBlock"
@update:modelValue="saveProfile()"
>
<template #label
>{{ i18n.ts.enableAutoSensitive
}}<span class="_beta">{{ i18n.ts.beta }}</span></template
>
<template #caption>{{
i18n.ts.enableAutoSensitiveDescription
}}</template>
</FormSwitch>
</FormSection>
</div>
</template>
@ -93,7 +80,6 @@ const usage = ref<any>(null);
const capacity = ref<any>(null);
const uploadFolder = ref<any>(null);
let alwaysMarkNsfw = $ref($i.alwaysMarkNsfw);
let autoSensitive = $ref($i.autoSensitive);
const meterStyle = computed(() => {
return {
@ -140,8 +126,7 @@ function chooseUploadFolder() {
function saveProfile() {
os.api("i/update", {
alwaysMarkNsfw: !!alwaysMarkNsfw,
autoSensitive: !!autoSensitive,
alwaysMarkNsfw: !!alwaysMarkNsfw
});
}

738
yarn.lock

File diff suppressed because it is too large Load Diff