merge: remove tensorflow, AiService (#140)

This commit is contained in:
Marie 2023-11-05 14:42:15 +01:00 committed by GitHub
commit ecf63c4333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 94 additions and 943 deletions

View File

@ -58,8 +58,5 @@
"cypress": "13.4.0", "cypress": "13.4.0",
"eslint": "8.52.0", "eslint": "8.52.0",
"start-server-and-test": "2.0.1" "start-server-and-test": "2.0.1"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -38,8 +38,6 @@
"@swc/core-win32-arm64-msvc": "1.3.56", "@swc/core-win32-arm64-msvc": "1.3.56",
"@swc/core-win32-ia32-msvc": "1.3.56", "@swc/core-win32-ia32-msvc": "1.3.56",
"@swc/core-win32-x64-msvc": "1.3.56", "@swc/core-win32-x64-msvc": "1.3.56",
"@tensorflow/tfjs": "4.4.0",
"@tensorflow/tfjs-node": "4.4.0",
"bufferutil": "4.0.7", "bufferutil": "4.0.7",
"slacc-android-arm-eabi": "0.0.10", "slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10", "slacc-android-arm64": "0.0.10",
@ -129,7 +127,6 @@
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.7", "nodemailer": "6.9.7",
"nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",

View File

@ -1,72 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Injectable } from '@nestjs/common';
import * as nsfw from 'nsfwjs';
import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import { bindThis } from '@/decorators.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const REQUIRED_CPU_FLAGS = ['avx2', 'fma'];
let isSupportedCpu: undefined | boolean = undefined;
@Injectable()
export class AiService {
private model: nsfw.NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor(
) {
}
@bindThis
public async detectSensitive(path: string): Promise<nsfw.predictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
const cpuFlags = await this.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 (this.model == null) {
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.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 this.model.classify(image);
return predictions;
} finally {
image.dispose();
}
} catch (err) {
console.error(err);
return null;
}
}
@bindThis
private async getCpuFlags(): Promise<string[]> {
const str = await si.cpuFlags();
return str.split(/\s+/);
}
}

View File

@ -6,7 +6,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AccountMoveService } from './AccountMoveService.js'; import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js'; import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
import { AnnouncementService } from './AnnouncementService.js'; import { AnnouncementService } from './AnnouncementService.js';
import { AntennaService } from './AntennaService.js'; import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js'; import { AppLockService } from './AppLockService.js';
@ -139,7 +138,6 @@ import type { Provider } from '@nestjs/common';
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService }; const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
@ -276,7 +274,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
LoggerService, LoggerService,
AccountMoveService, AccountMoveService,
AccountUpdateService, AccountUpdateService,
AiService,
AnnouncementService, AnnouncementService,
AntennaService, AntennaService,
AppLockService, AppLockService,
@ -406,7 +403,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$LoggerService, $LoggerService,
$AccountMoveService, $AccountMoveService,
$AccountUpdateService, $AccountUpdateService,
$AiService,
$AnnouncementService, $AnnouncementService,
$AntennaService, $AntennaService,
$AppLockService, $AppLockService,
@ -537,7 +533,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
LoggerService, LoggerService,
AccountMoveService, AccountMoveService,
AccountUpdateService, AccountUpdateService,
AiService,
AnnouncementService, AnnouncementService,
AntennaService, AntennaService,
AppLockService, AppLockService,
@ -666,7 +661,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$LoggerService, $LoggerService,
$AccountMoveService, $AccountMoveService,
$AccountUpdateService, $AccountUpdateService,
$AiService,
$AnnouncementService, $AnnouncementService,
$AntennaService, $AntennaService,
$AppLockService, $AppLockService,

View File

@ -461,36 +461,12 @@ export class DriveService {
requestHeaders = null, requestHeaders = null,
ext = null, ext = null,
}: AddFileArgs): Promise<MiDriveFile> { }: AddFileArgs): Promise<MiDriveFile> {
let skipNsfwCheck = false;
const instance = await this.metaService.fetch(); const instance = await this.metaService.fetch();
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
if (user == null) {
skipNsfwCheck = true;
} else if (userRoleNSFW) {
skipNsfwCheck = true;
}
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
const info = await this.fileInfoService.getFileInfo(path, { const info = await this.fileInfoService.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,
});
this.registerLogger.info(`${JSON.stringify(info)}`); this.registerLogger.info(`${JSON.stringify(info)}`);
// 現状 false positive が多すぎて実用に耐えない
//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) {
// throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
//}
// detect name // detect name
const detectedName = correctFilename( const detectedName = correctFilename(
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、 // DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
@ -586,7 +562,6 @@ export class DriveService {
: false; : false;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
if (userRoleNSFW) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true;
if (url !== null) { if (url !== null) {

View File

@ -5,19 +5,13 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import { join } from 'node:path';
import * as stream from 'node:stream/promises'; import * as stream from 'node:stream/promises';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { FSWatcher } from 'chokidar';
import * as fileType from 'file-type'; import * as fileType from 'file-type';
import FFmpeg from 'fluent-ffmpeg';
import isSvg from 'is-svg'; import isSvg from 'is-svg';
import probeImageSize from 'probe-image-size'; import probeImageSize from 'probe-image-size';
import { type predictionType } from 'nsfwjs';
import sharp from 'sharp'; import sharp from 'sharp';
import { encode } from 'blurhash'; import { encode } from 'blurhash';
import { createTempDir } from '@/misc/create-temp.js';
import { AiService } from '@/core/AiService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
export type FileInfo = { export type FileInfo = {
@ -49,7 +43,6 @@ const TYPE_SVG = {
@Injectable() @Injectable()
export class FileInfoService { export class FileInfoService {
constructor( constructor(
private aiService: AiService,
) { ) {
} }
@ -57,12 +50,7 @@ export class FileInfoService {
* Get file information * Get file information
*/ */
@bindThis @bindThis
public async getFileInfo(path: string, opts: { public async getFileInfo(path: string): Promise<FileInfo> {
skipSensitiveDetection: boolean;
sensitiveThreshold?: number;
sensitiveThresholdForPorn?: number;
enableSensitiveMediaDetectionForVideos?: boolean;
}): Promise<FileInfo> {
const warnings = [] as string[]; const warnings = [] as string[];
const size = await this.getFileSize(path); const size = await this.getFileSize(path);
@ -128,22 +116,8 @@ export class FileInfoService {
}); });
} }
let sensitive = false; const sensitive = false;
let porn = false; const porn = false;
if (!opts.skipSensitiveDetection) {
await this.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 { return {
size, size,
@ -159,150 +133,6 @@ export class FileInfoService {
}; };
} }
@bindThis
private async 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 this.aiService.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 this.asyncIterateFrames(outDir, command)) {
try {
const index = frameIndex++;
if (index !== targetIndex) {
continue;
}
targetIndex = nextIndex;
nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける
const result = await this.aiService.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];
}
private async *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++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
const current = `${i}.png`;
const next = `${i + 1}.png`;
const framePath = join(cwd, current);
if (await this.exists(join(cwd, next))) {
yield framePath;
} else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
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 this.exists(framePath)) {
yield framePath;
} else {
return;
}
}
}
@bindThis
private exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(() => true, () => false);
}
@bindThis @bindThis
public fixMime(mime: string | fileType.MimeType): string { public fixMime(mime: string | fileType.MimeType): string {
// see https://github.com/misskey-dev/misskey/pull/10686 // see https://github.com/misskey-dev/misskey/pull/10686

View File

@ -292,22 +292,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.turnstileSecretKey = ps.turnstileSecretKey; set.turnstileSecretKey = ps.turnstileSecretKey;
} }
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.enableBotTrending !== undefined) { if (ps.enableBotTrending !== undefined) {
set.enableBotTrending = ps.enableBotTrending; set.enableBotTrending = ps.enableBotTrending;
} }

View File

@ -14,7 +14,6 @@ import { describe, beforeAll, afterAll, test } from '@jest/globals';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { FileInfoService } from '@/core/FileInfoService.js'; import { FileInfoService } from '@/core/FileInfoService.js';
//import { DI } from '@/di-symbols.js'; //import { DI } from '@/di-symbols.js';
import { AiService } from '@/core/AiService.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
@ -34,14 +33,10 @@ describe('FileInfoService', () => {
GlobalModule, GlobalModule,
], ],
providers: [ providers: [
AiService,
FileInfoService, FileInfoService,
], ],
}) })
.useMocker((token) => { .useMocker((token) => {
//if (token === AiService) {
// return { };
//}
if (typeof token === 'function') { if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>; const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata); const Mock = moduleMocker.generateFromMetadata(mockMetadata);
@ -61,7 +56,7 @@ describe('FileInfoService', () => {
test('Empty file', async () => { test('Empty file', async () => {
const path = `${resources}/emptyfile`; const path = `${resources}/emptyfile`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -82,7 +77,7 @@ describe('FileInfoService', () => {
describe('IMAGE', () => { describe('IMAGE', () => {
test('Generic JPEG', async () => { test('Generic JPEG', async () => {
const path = `${resources}/Lenna.jpg`; const path = `${resources}/Lenna.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -102,7 +97,7 @@ describe('FileInfoService', () => {
test('Generic APNG', async () => { test('Generic APNG', async () => {
const path = `${resources}/anime.png`; const path = `${resources}/anime.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -122,7 +117,7 @@ describe('FileInfoService', () => {
test('Generic AGIF', async () => { test('Generic AGIF', async () => {
const path = `${resources}/anime.gif`; const path = `${resources}/anime.gif`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -142,7 +137,7 @@ describe('FileInfoService', () => {
test('PNG with alpha', async () => { test('PNG with alpha', async () => {
const path = `${resources}/with-alpha.png`; const path = `${resources}/with-alpha.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -162,7 +157,7 @@ describe('FileInfoService', () => {
test('Generic SVG', async () => { test('Generic SVG', async () => {
const path = `${resources}/image.svg`; const path = `${resources}/image.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -183,7 +178,7 @@ describe('FileInfoService', () => {
test('SVG with XML definition', async () => { test('SVG with XML definition', async () => {
// https://github.com/misskey-dev/misskey/issues/4413 // https://github.com/misskey-dev/misskey/issues/4413
const path = `${resources}/with-xml-def.svg`; const path = `${resources}/with-xml-def.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -203,7 +198,7 @@ describe('FileInfoService', () => {
test('Dimension limit', async () => { test('Dimension limit', async () => {
const path = `${resources}/25000x25000.png`; const path = `${resources}/25000x25000.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -223,7 +218,7 @@ describe('FileInfoService', () => {
test('Rotate JPEG', async () => { test('Rotate JPEG', async () => {
const path = `${resources}/rotate.jpg`; const path = `${resources}/rotate.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -245,7 +240,7 @@ describe('FileInfoService', () => {
describe('AUDIO', () => { describe('AUDIO', () => {
test('MP3', async () => { test('MP3', async () => {
const path = `${resources}/kick_gaba7.mp3`; const path = `${resources}/kick_gaba7.mp3`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -265,7 +260,7 @@ describe('FileInfoService', () => {
test('WAV', async () => { test('WAV', async () => {
const path = `${resources}/kick_gaba7.wav`; const path = `${resources}/kick_gaba7.wav`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -285,7 +280,7 @@ describe('FileInfoService', () => {
test('AAC', async () => { test('AAC', async () => {
const path = `${resources}/kick_gaba7.aac`; const path = `${resources}/kick_gaba7.aac`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -305,7 +300,7 @@ describe('FileInfoService', () => {
test('FLAC', async () => { test('FLAC', async () => {
const path = `${resources}/kick_gaba7.flac`; const path = `${resources}/kick_gaba7.flac`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
@ -327,7 +322,7 @@ describe('FileInfoService', () => {
* video/webmとして検出されてしまう * video/webmとして検出されてしまう
test('WEBM AUDIO', async () => { test('WEBM AUDIO', async () => {
const path = `${resources}/kick_gaba7.webm`; const path = `${resources}/kick_gaba7.webm`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;

View File

@ -20,49 +20,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<XBotProtection/> <XBotProtection/>
</MkFolder> </MkFolder>
<MkFolder>
<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="_gaps_m">
<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
<MkRadios v-model="sensitiveMediaDetection">
<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>
</MkRadios>
<MkRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
</MkRange>
<MkSwitch v-model="enableSensitiveMediaDetectionForVideos">
<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
</MkSwitch>
<MkSwitch v-model="setSensitiveFlagAutomatically">
<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
</MkSwitch>
<!-- 現状 false positive が多すぎて実用に耐えない
<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
</MkSwitch>
-->
<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #label>Active Email Validation</template> <template #label>Active Email Validation</template>
<template v-if="enableActiveEmailValidation" #suffix>Enabled</template> <template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
@ -126,10 +83,6 @@ let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false); let enableHcaptcha: boolean = $ref(false);
let enableRecaptcha: boolean = $ref(false); let enableRecaptcha: boolean = $ref(false);
let enableTurnstile: boolean = $ref(false); let enableTurnstile: 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 enableIpLogging: boolean = $ref(false);
let enableActiveEmailValidation: boolean = $ref(false); let enableActiveEmailValidation: boolean = $ref(false);
@ -139,15 +92,6 @@ async function init() {
enableHcaptcha = meta.enableHcaptcha; enableHcaptcha = meta.enableHcaptcha;
enableRecaptcha = meta.enableRecaptcha; enableRecaptcha = meta.enableRecaptcha;
enableTurnstile = meta.enableTurnstile; enableTurnstile = meta.enableTurnstile;
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; enableIpLogging = meta.enableIpLogging;
enableActiveEmailValidation = meta.enableActiveEmailValidation; enableActiveEmailValidation = meta.enableActiveEmailValidation;
} }
@ -155,16 +99,6 @@ async function init() {
function save() { function save() {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
summalyProxy, summalyProxy,
sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity:
sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' :
sensitiveMediaDetectionSensitivity === 1 ? 'low' :
sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
sensitiveMediaDetectionSensitivity === 3 ? 'high' :
sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
0,
setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos,
enableIpLogging, enableIpLogging,
enableActiveEmailValidation, enableActiveEmailValidation,
}).then(() => { }).then(() => {

File diff suppressed because it is too large Load Diff