diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-07 21:06:37 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-07-07 21:06:37 +0900 |
| commit | e560601815be226d713b97c96591b6e83b85578b (patch) | |
| tree | 03f3bb0a4c31389991dd1846b10b20ed30fbcc0c /packages/client/src | |
| parent | New Crowdin updates (#8950) (diff) | |
| download | misskey-e560601815be226d713b97c96591b6e83b85578b.tar.gz misskey-e560601815be226d713b97c96591b6e83b85578b.tar.bz2 misskey-e560601815be226d713b97c96591b6e83b85578b.zip | |
feat: auto nsfw detection (#8840)
* feat: auto nsfw detection
* :v:
* Update ja-JP.yml
* Update ja-JP.yml
* ポルノ判定のしきい値を高めに
* エラーハンドリングちゃんとした
* Update ja-JP.yml
* 感度設定を強化
* refactor
* feat: add video support for auto nsfw detection
* rename: image -> media
* .js
* fix: add missing error handling
* fix: use valid pathname instead of using filename due to invalid usage
* perf(nsfw-detection): decode frames
* disable detection of video for some reasons
* perf(nsfw-detection): streamify detection process for video
* disable disallowUploadWhenPredictedAsPorn option
* fix(nsfw-detection): improve reliability
* fix(nsfw-detection): use Math.ceil instead of Math.round
* perf(nsfw-detection): delete tmp frames after used
* fix(nsfw-detection): FSWatcher does not emit ready event
* perf(nsfw-detection): skip black frames
* refactor: strip exists check
* Update package.json
* めっちゃ変えた
* lint
* Update COPYING
* オプションで動画解析できるように
* Update yarn.lock
* Update CHANGELOG.md
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Diffstat (limited to 'packages/client/src')
| -rw-r--r-- | packages/client/src/pages/admin/security.vue | 69 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/drive.vue | 22 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/profile.vue | 4 | ||||
| -rw-r--r-- | packages/client/src/scripts/select-file.ts | 23 | ||||
| -rw-r--r-- | packages/client/src/scripts/upload.ts | 36 | ||||
| -rw-r--r-- | packages/client/src/style.scss | 10 |
6 files changed, 139 insertions, 25 deletions
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue index 76fa9d21eb..c4a4994bb8 100644 --- a/packages/client/src/pages/admin/security.vue +++ b/packages/client/src/pages/admin/security.vue @@ -15,6 +15,49 @@ </FormFolder> <FormFolder class="_formBlock"> + <template #icon><i class="fas fa-eye-slash"></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="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> + </div> + </FormFolder> + + <FormFolder class="_formBlock"> <template #label>Log IP address</template> <template v-if="enableIpLogging" #suffix>Enabled</template> <template v-else #suffix>Disabled</template> @@ -49,10 +92,11 @@ import { } from 'vue'; import XBotProtection from './bot-protection.vue'; import XHeader from './_header_.vue'; import FormFolder from '@/components/form/folder.vue'; +import FormRadios from '@/components/form/radios.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInfo from '@/components/ui/info.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import FormSection from '@/components/form/section.vue'; +import FormRange from '@/components/form/range.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; @@ -63,6 +107,10 @@ 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); async function init() { @@ -70,12 +118,31 @@ 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; } 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, }).then(() => { fetchInstance(); diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index cec2dc4d5f..c8c78f2923 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -28,7 +28,17 @@ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="fas fa-folder-open"></i></template> </FormLink> - <FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ i18n.ts.keepOriginalUploading }}<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template></FormSwitch> + <FormSwitch v-model="keepOriginalUploading" class="_formBlock"> + <template #label>{{ i18n.ts.keepOriginalUploading }}</template> + <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> + </FormSwitch> + <FormSwitch v-model="alwaysMarkNsfw" class="_formBlock" @update:modelValue="saveProfile()"> + <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> @@ -47,11 +57,14 @@ import { defaultStore } from '@/store'; import MkChart from '@/components/chart.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { $i } from '@/account'; const fetching = ref(true); 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 { @@ -94,6 +107,13 @@ function chooseUploadFolder() { }); } +function saveProfile() { + os.api('i/update', { + alwaysMarkNsfw: !!alwaysMarkNsfw, + autoSensitive: !!autoSensitive, + }); +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index cbc7327233..2a326fc2b6 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -56,8 +56,6 @@ <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch> <FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch> <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch> - - <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch> </div> </template> @@ -88,7 +86,6 @@ const profile = reactive({ isBot: $i.isBot, isCat: $i.isCat, showTimelineReplies: $i.showTimelineReplies, - alwaysMarkNsfw: $i.alwaysMarkNsfw, }); watch(() => profile, () => { @@ -126,7 +123,6 @@ function save() { isBot: !!profile.isBot, isCat: !!profile.isCat, showTimelineReplies: !!profile.showTimelineReplies, - alwaysMarkNsfw: !!profile.alwaysMarkNsfw, }); } diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 461d613b42..17e31d96f1 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -1,9 +1,9 @@ import { ref } from 'vue'; +import { DriveFile } from 'misskey-js/built/entities'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; -import { DriveFile } from 'misskey-js/built/entities'; import { uploadFile } from '@/scripts/upload'; function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { @@ -20,10 +20,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); }).catch(err => { - os.alert({ - type: 'error', - text: err - }); + // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない }); // 一応廃棄 @@ -47,7 +44,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv os.inputText({ title: i18n.ts.uploadFromUrl, type: 'url', - placeholder: i18n.ts.uploadFromUrlDescription + placeholder: i18n.ts.uploadFromUrlDescription, }).then(({ canceled, result: url }) => { if (canceled) return; @@ -64,35 +61,35 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv os.api('drive/files/upload-from-url', { url: url, folderId: defaultStore.state.uploadFolder, - marker + marker, }); os.alert({ title: i18n.ts.uploadFromUrlRequested, - text: i18n.ts.uploadFromUrlMayTakeTime + text: i18n.ts.uploadFromUrlMayTakeTime, }); }); }; os.popupMenu([label ? { text: label, - type: 'label' + type: 'label', } : undefined, { type: 'switch', text: i18n.ts.keepOriginalUploading, - ref: keepOriginal + ref: keepOriginal, }, { text: i18n.ts.upload, icon: 'fas fa-upload', - action: chooseFileFromPc + action: chooseFileFromPc, }, { text: i18n.ts.fromDrive, icon: 'fas fa-cloud', - action: chooseFileFromDrive + action: chooseFileFromDrive, }, { text: i18n.ts.fromUrl, icon: 'fas fa-link', - action: chooseFileFromUrl + action: chooseFileFromUrl, }], src); }); } diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts index 2f907e5e84..51f1c1b86f 100644 --- a/packages/client/src/scripts/upload.ts +++ b/packages/client/src/scripts/upload.ts @@ -5,6 +5,7 @@ import { defaultStore } from '@/store'; import { apiUrl } from '@/config'; import { $i } from '@/account'; import { alert } from '@/os'; +import { i18n } from '@/i18n'; type Uploading = { id: string; @@ -80,14 +81,37 @@ export function uploadFile( xhr.open('POST', apiUrl + '/drive/files/create', true); xhr.onload = (ev) => { if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて再送できるようにしたい + // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい uploads.value = uploads.value.filter(x => x.id !== id); - alert({ - type: 'error', - title: 'Failed to upload', - text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, - }); + if (ev.target?.response) { + const res = JSON.parse(ev.target.response); + if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseInappropriate, + }); + } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseNoFreeSpace, + }); + } else { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, + }); + } + } else { + alert({ + type: 'error', + title: 'Failed to upload', + text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, + }); + } reject(); return; diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss index 6f569bba1b..9b30e64a31 100644 --- a/packages/client/src/style.scss +++ b/packages/client/src/style.scss @@ -399,6 +399,16 @@ hr { } } +._beta { + margin-left: 0.7em; + font-size: 65%; + padding: 2px 3px; + color: var(--accent); + border: solid 1px var(--accent); + border-radius: 4px; + vertical-align: top; +} + ._table { > ._row { display: flex; |