mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-23 02:03:08 +02:00
merge: remove tensorflow, AiService (#140)
This commit is contained in:
commit
ecf63c4333
17 changed files with 94 additions and 943 deletions
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
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
|
@ -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",
|
||||||
|
|
|
@ -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+/);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
605
pnpm-lock.yaml
605
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue