This commit is contained in:
MomentQYC 2024-10-20 19:06:20 +09:00 committed by GitHub
commit 82e5d57503
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1119 additions and 85 deletions

20
locales/index.d.ts vendored
View file

@ -730,6 +730,14 @@ export interface Locale extends ILocale {
*
*/
"flagAsCatDescription": string;
/**
* TTS機能が欲しい
*/
"flagAsVI": string;
/**
* TTS機能が必要な場合は有効にしてください TTS機能を有効にします
*/
"flagAsVIDescription": string;
/**
*
*/
@ -6891,6 +6899,10 @@ export interface Locale extends ILocale {
*
*/
"canUseTranslator": string;
/**
* TTS機能の利用
*/
"canUseTTS": string;
/**
*
*/
@ -6937,6 +6949,10 @@ export interface Locale extends ILocale {
* botユーザー
*/
"isBot": string;
/**
* TTSユーザー
*/
"isVI": string;
/**
*
*/
@ -7328,6 +7344,10 @@ export interface Locale extends ILocale {
* Misskeyを翻訳
*/
"translation": string;
/**
* Misskeyを変換
*/
"convert": string;
/**
* Misskeyに寄付
*/

View file

@ -178,6 +178,8 @@ flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
flagAsCatDescription: "にゃにゃにゃ??"
flagAsVI: "自動TTS機能が欲しい。"
flagAsVIDescription: "自動TTS機能が必要な場合は有効にしてください。 権限のあるユーザーグループに所属している場合、特定の範囲で自動TTS機能を有効にします。"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
@ -1780,6 +1782,7 @@ _role:
canHideAds: "広告の非表示"
canSearchNotes: "ノート検索の利用"
canUseTranslator: "翻訳機能の利用"
canUseTTS: "TTS機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
canImportAntennas: "アンテナのインポートを許可"
canImportBlocking: "ブロックのインポートを許可"
@ -1792,6 +1795,7 @@ _role:
isRemote: "リモートユーザー"
isCat: "猫ユーザー"
isBot: "botユーザー"
isVI: "TTSユーザー"
isSuspended: "サスペンド済みユーザー"
isLocked: "鍵アカウントユーザー"
isExplorable: "「アカウントを見つけやすくする」が有効なユーザー"
@ -1910,6 +1914,7 @@ _aboutMisskey:
original: "オリジナル"
thisIsModifiedVersion: "{name}はオリジナルのMisskeyを改変したバージョンを使用しています。"
translation: "Misskeyを翻訳"
convert: "Misskeyを変換"
donate: "Misskeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
patrons: "支援者"

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class TTSIntegration1724683952000 {
constructor() {
this.name = 'TTSIntegration1724683952000';
}
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "hfAuthKey" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfAuthKey"`);
}
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class TTSIntegration1724683962000 {
constructor() {
this.name = 'TTSIntegration1724683962000';
}
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "isVI" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isVI" IS 'Whether the User needs auto TTS.'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfSpace" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfSpaceName" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfexampleAudioURL" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfexampleText" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfexampleLang" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfslice" character varying(128) DEFAULT 'Slice once every 4 sentences'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hftopK" INTEGER DEFAULT 15`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hftopP" INTEGER DEFAULT 100`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfTemperature" INTEGER DEFAULT 100`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfnrm" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfSpeedRate" INTEGER DEFAULT 125`);
await queryRunner.query(`ALTER TABLE "meta" ADD "hfdas" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."isVI" IS NULL`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVI"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfSpace"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfSpaceName"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfexampleAudioURL"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfexampleText"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfexampleLang"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfslice"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hftopK"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hftopP"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfTemperature"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfnrm"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfSpeedRate"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hfdas"`);
}
}

View file

@ -81,6 +81,7 @@
"@fastify/multipart": "9.0.1",
"@fastify/static": "8.0.1",
"@fastify/view": "10.0.1",
"@gradio/client": "1.6.0-beta.3",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.56",
@ -134,8 +135,8 @@
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"meilisearch": "0.42.0",
"juice": "11.0.0",
"meilisearch": "0.42.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",

View file

@ -44,6 +44,7 @@ export type RolePolicies = {
canManageAvatarDecorations: boolean;
canSearchNotes: boolean;
canUseTranslator: boolean;
canUseTTS: boolean;
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
@ -78,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canManageAvatarDecorations: false,
canSearchNotes: false,
canUseTranslator: true,
canUseTTS: true,
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
@ -256,6 +258,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
case 'isCat': {
return user.isCat;
}
// Auto TTS
case 'isVI': {
return user.isVI;
}
// 「ユーザを見つけやすくする」が有効なアカウント
case 'isExplorable': {
return user.isExplorable;
@ -383,6 +389,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
canUseTTS: calc('canUseTTS', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),

View file

@ -79,6 +79,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isLocked: false,
isBot: false,
isCat: true,
isVI: false,
isRoot: false,
isExplorable: true,
isHibernated: false,
@ -198,6 +199,7 @@ function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'
})),
isBot: user.isBot,
isCat: user.isCat,
isVI: user.isVI,
emojis: user.emojis,
onlineStatus: 'active',
badgeRoles: [],

View file

@ -123,6 +123,7 @@ export class MetaEntityService {
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
ttsAvailable: instance.hfAuthKey != null,
serverRules: instance.serverRules,

View file

@ -490,6 +490,7 @@ export class UserEntityService implements OnModuleInit {
}))) : [],
isBot: user.isBot,
isCat: user.isCat,
isVI: user.isVI,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,

View file

@ -359,6 +359,78 @@ export class MiMeta {
})
public deeplIsPro: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public hfAuthKey: string | null;
@Column('boolean', {
default: false,
})
public hfSpace: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public hfSpaceName: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public hfexampleAudioURL: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public hfexampleText: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public hfexampleLang: string | null;
@Column('varchar', {
length: 1024,
default: 'Slice once every 4 sentences',
nullable: true,
})
public hfslice: string | null;
@Column('integer', {
default: 15,
})
public hftopK: number;
@Column('integer', {
default: 100,
})
public hftopP: number;
@Column('integer', {
default: 100,
})
public hfTemperature: number;
@Column('boolean', {
default: false,
})
public hfnrm: boolean;
@Column('integer', {
default: 125,
})
public hfSpeedRate: number;
@Column('boolean', {
default: false,
})
public hfdas: boolean;
@Column('varchar', {
length: 1024,
nullable: true,

View file

@ -83,6 +83,13 @@ type CondFormulaValueIsCat = {
type: 'isCat';
};
/**
* Auto TTS
*/
type CondFormulaValueIsVI = {
type: 'isVI';
};
/**
*
*/
@ -164,6 +171,7 @@ export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueIsLocked |
CondFormulaValueIsBot |
CondFormulaValueIsCat |
CondFormulaValueIsVI |
CondFormulaValueIsExplorable |
CondFormulaValueRoleAssignedTo |
CondFormulaValueCreatedLessThan |

View file

@ -184,6 +184,12 @@ export class MiUser {
})
public isCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User needs auto TTS.',
})
public isVI: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is the root.',

View file

@ -207,6 +207,10 @@ export const packedMetaLiteSchema = {
type: 'boolean',
optional: false, nullable: false,
},
ttsAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,

View file

@ -66,7 +66,7 @@ export const packedRoleCondFormulaValueUserSettingBooleanSchema = {
type: {
type: 'string',
nullable: false, optional: false,
enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isExplorable'],
enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isVI', 'isExplorable'],
},
},
} as const;
@ -216,6 +216,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canUseTTS: {
type: 'boolean',
optional: false, nullable: false,
},
canHideAds: {
type: 'boolean',
optional: false, nullable: false,

View file

@ -115,6 +115,10 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: true,
},
isVI: {
type: 'boolean',
nullable: false, optional: true,
},
instance: {
type: 'object',
nullable: false, optional: true,

View file

@ -305,6 +305,7 @@ import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting
import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js';
import * as ep___notes_timeline from './endpoints/notes/timeline.js';
import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_tts from './endpoints/notes/tts.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
@ -692,6 +693,7 @@ const $notes_threadMuting_create: Provider = { provide: 'ep:notes/thread-muting/
const $notes_threadMuting_delete: Provider = { provide: 'ep:notes/thread-muting/delete', useClass: ep___notes_threadMuting_delete.default };
const $notes_timeline: Provider = { provide: 'ep:notes/timeline', useClass: ep___notes_timeline.default };
const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep___notes_translate.default };
const $notes_tts: Provider = { provide: 'ep:notes/tts', useClass: ep___notes_tts.default };
const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default };
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
@ -1083,6 +1085,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_threadMuting_delete,
$notes_timeline,
$notes_translate,
$notes_tts,
$notes_unrenote,
$notes_userListTimeline,
$notifications_create,
@ -1468,6 +1471,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_threadMuting_delete,
$notes_timeline,
$notes_translate,
$notes_tts,
$notes_unrenote,
$notes_userListTimeline,
$notifications_create,

View file

@ -311,6 +311,7 @@ import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting
import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js';
import * as ep___notes_timeline from './endpoints/notes/timeline.js';
import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_tts from './endpoints/notes/tts.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
@ -696,6 +697,7 @@ const eps = [
['notes/thread-muting/delete', ep___notes_threadMuting_delete],
['notes/timeline', ep___notes_timeline],
['notes/translate', ep___notes_translate],
['notes/tts', ep___notes_tts],
['notes/unrenote', ep___notes_unrenote],
['notes/user-list-timeline', ep___notes_userListTimeline],
['notifications/create', ep___notifications_create],

View file

@ -122,6 +122,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
ttsAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
silencedHosts: {
type: 'array',
optional: true,
@ -412,6 +416,58 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
hfAuthKey: {
type: 'string',
optional: false, nullable: true,
},
hfSpace: {
type: 'boolean',
optional: false, nullable: false,
},
hfSpaceName: {
type: 'string',
optional: false, nullable: true,
},
hfexampleAudioURL: {
type: 'string',
optional: false, nullable: true,
},
hfexampleText: {
type: 'string',
optional: false, nullable: true,
},
hfexampleLang: {
type: 'string',
optional: false, nullable: true,
},
hfslice: {
type: 'string',
optional: false, nullable: true,
},
hftopK: {
type: 'number',
optional: false, nullable: true,
},
hftopP: {
type: 'number',
optional: false, nullable: true,
},
hfTemperature: {
type: 'number',
optional: false, nullable: true,
},
hfSpeedRate: {
type: 'number',
optional: false, nullable: true,
},
hfnrm: {
type: 'boolean',
optional: false, nullable: false,
},
hfdas: {
type: 'boolean',
optional: false, nullable: false,
},
defaultDarkTheme: {
type: 'string',
optional: false, nullable: true,
@ -588,6 +644,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
ttsAvailable: instance.hfAuthKey != null,
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
pinnedUsers: instance.pinnedUsers,
@ -630,6 +687,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro,
hfAuthKey: instance.hfAuthKey,
hfSpace: instance.hfSpace,
hfSpaceName: instance.hfSpaceName,
hfexampleAudioURL: instance.hfexampleAudioURL,
hfexampleText: instance.hfexampleText,
hfexampleLang: instance.hfexampleLang,
hfslice: instance.hfslice,
hftopK: instance.hftopK,
hftopP: instance.hftopP,
hfTemperature: instance.hfTemperature,
hfSpeedRate: instance.hfSpeedRate,
hfnrm: instance.hfnrm,
hfdas: instance.hfdas,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableVerifymailApi: instance.enableVerifymailApi,

View file

@ -98,6 +98,19 @@ export const paramDef = {
},
deeplAuthKey: { type: 'string', nullable: true },
deeplIsPro: { type: 'boolean' },
hfAuthKey: { type: 'string', nullable: true },
hfSpace: { type: 'boolean', default: false },
hfSpaceName: { type: 'string', nullable: true },
hfexampleAudioURL: { type: 'string', nullable: true },
hfexampleText: { type: 'string', nullable: true },
hfexampleLang: { type: 'string', nullable: true },
hfslice: { type: 'string', default: 'Slice once every 4 sentences', nullable: true },
hftopK: { type: 'integer', default: 15 },
hftopP: { type: 'integer', default: 100 },
hfTemperature: { type: 'integer', default: 100 },
hfnrm: { type: 'boolean', default: false },
hfSpeedRate: { type: 'integer', default: 125 },
hfdas: { type: 'boolean', default: false },
enableEmail: { type: 'boolean' },
email: { type: 'string', nullable: true },
smtpSecure: { type: 'boolean' },
@ -531,6 +544,82 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.deeplIsPro = ps.deeplIsPro;
}
if (ps.hfAuthKey !== undefined) {
if (ps.hfAuthKey === '') {
set.hfAuthKey = null;
} else {
set.hfAuthKey = ps.hfAuthKey;
}
}
if (ps.hfSpace !== undefined) {
set.hfSpace = ps.hfSpace;
}
if (ps.hfSpaceName !== undefined) {
if (ps.hfSpaceName === '') {
set.hfSpaceName = null;
} else {
set.hfSpaceName = ps.hfSpaceName;
}
}
if (ps.hfexampleAudioURL !== undefined) {
if (ps.hfexampleAudioURL === '') {
set.hfexampleAudioURL = null;
} else {
set.hfexampleAudioURL = ps.hfexampleAudioURL;
}
}
if (ps.hfexampleText !== undefined) {
if (ps.hfexampleText === '') {
set.hfexampleText = null;
} else {
set.hfexampleText = ps.hfexampleText;
}
}
if (ps.hfexampleLang !== undefined) {
if (ps.hfexampleLang === '') {
set.hfexampleLang = null;
} else {
set.hfexampleLang = ps.hfexampleLang;
}
}
if (ps.hfslice !== undefined) {
if (ps.hfslice === '') {
set.hfslice = null;
} else {
set.hfslice = ps.hfslice;
}
}
if (ps.hftopK !== undefined) {
set.hftopK = ps.hftopK;
}
if (ps.hftopP !== undefined) {
set.hftopP = ps.hftopP;
}
if (ps.hfTemperature !== undefined) {
set.hfTemperature = ps.hfTemperature;
}
if (ps.hfnrm !== undefined) {
set.hfnrm = ps.hfnrm;
}
if (ps.hfSpeedRate !== undefined) {
set.hfSpeedRate = ps.hfSpeedRate;
}
if (ps.hfdas !== undefined) {
set.hfdas = ps.hfdas;
}
if (ps.enableIpLogging !== undefined) {
set.enableIpLogging = ps.enableIpLogging;
}

View file

@ -181,6 +181,7 @@ export const paramDef = {
preventAiLearning: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
isVI: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
@ -335,6 +336,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.isVI === 'boolean') updates.isVI = ps.isVI;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') {

View file

@ -0,0 +1,197 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Client } from "@gradio/client";
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
kind: 'read:account',
res: {
type: 'string',
optional: true, nullable: false,
contentMediaType: 'audio/flac',
},
errors: {
incorrectconfig: {
message: 'Incorrect configuration.',
code: 'INCORRECT_CONFIG',
id: '8d171e60-83b8-11ef-b98c-a7506d6c1de4',
},
unavailable: {
message: 'Convert of notes unavailable.',
code: 'UNAVAILABLE',
id: '97a0826c-6393-11ef-a650-67972d710975',
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'bea9b03f-36e0-49c5-a4db-627a029f8971',
},
cannotConvertInvisibleNote: {
message: 'Cannot convert invisible note.',
code: 'CANNOT_CONVERT_INVISIBLE_NOTE',
id: 'f57caae0-6394-11ef-8e2a-d706932c1030',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['noteId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private noteEntityService: NoteEntityService,
private getterService: GetterService,
private metaService: MetaService,
private httpRequestService: HttpRequestService,
private roleService: RoleService,
) {
// @ts-expect-error: Functionality can be implemented here with minimal modifications.
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.canUseTTS) {
throw new ApiError(meta.errors.unavailable);
}
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) {
throw new ApiError(meta.errors.cannotConvertInvisibleNote);
}
if (note.text == null) {
throw new ApiError(meta.errors.cannotConvertInvisibleNote);
}
const instance = await this.metaService.fetch();
if (instance.hfAuthKey == null) {
throw new ApiError(meta.errors.unavailable);
}
let outofQuota;
if (instance.hfSpace) {
const langlist = ['Chinese', 'English', 'Japanese', 'Yue', 'Korean', 'Chinese-English Mixed', 'Japanese-English Mixed', 'Yue-English Mixed', 'Korean-English Mixed', 'Multilingual Mixed', 'Multilingual Mixed(Yue)'];
const slicelist = ['No slice', 'Slice once every 4 sentences', 'Slice per 50 characters', 'Slice by Chinese punct', 'Slice by English punct', 'Slice by every punct'];
let exampleAudio;
let app;
try {
const example = await fetch(instance.hfexampleAudioURL || '');
exampleAudio = await example.blob();
} catch (e) {
throw new ApiError(meta.errors.unavailable);
}
if (((!instance.hfnrm) && (!instance.hfexampleText)) || (!langlist.includes(instance.hfexampleLang || '')) || (!slicelist.includes(instance.hfslice || '')) || (!instance.hfSpaceName) || (!(instance.hfSpeedRate >= 60 && instance.hfSpeedRate <= 165)) || (!(instance.hfTemperature >= 0 && instance.hfTemperature <= 100)) || (!(instance.hftopK >= 0 && instance.hftopK <= 100)) || (!(instance.hftopP >= 0 && instance.hftopP <= 100))) {
throw new ApiError(meta.errors.incorrectconfig);
}
try {
app = await Client.connect(instance.hfSpaceName, { hf_token: instance.hfAuthKey });
} catch (e) {
throw new ApiError(meta.errors.unavailable);
}
let result;
let notcontinue;
try {
result = await app.predict("/get_tts_wav", [
exampleAudio,
instance.hfexampleText,
instance.hfexampleLang,
note.text,
"Multilingual Mixed",
instance.hfslice,
instance.hftopK,
instance.hftopP / 100,
instance.hfTemperature / 100,
instance.hfnrm,
instance.hfSpeedRate / 100,
instance.hfdas,
]);
} catch (e) {
const responseMessage = (e as any).message || ((e as any).original_msg && (e as any).original_msg.message);
if (responseMessage && responseMessage.includes('You have exceeded your GPU quota')) {
outofQuota = true;
console.info("Fallback to Inference API");
notcontinue = true;
} else {
throw new ApiError(meta.errors.unavailable);
}
}
if (!notcontinue) {
const resurl = result.data[0].url;
const res = await this.httpRequestService.send(resurl, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + instance.hfAuthKey,
},
timeout: 60000,
});
const contentType = res.headers.get('Content-Type') || 'application/octet-stream';
if (contentType === 'audio/x-wav') {
return res.body;
} else {
throw new ApiError(meta.errors.unavailable);
}
}
}
if ((!instance.hfSpace) || ((instance.hfSpace) && (outofQuota))) {
const endpoint = 'https://api-inference.huggingface.co/models/suno/bark';
const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + instance.hfAuthKey,
Accept: 'audio/flac, */*',
},
body: JSON.stringify({
inputs: note.text,
}),
timeout: 60000,
});
const contentType = res.headers.get('Content-Type') || 'application/octet-stream';
if (contentType === 'audio/flac') {
return res.body;
} else {
throw new ApiError(meta.errors.unavailable);
}
}
});
}
}

View file

@ -87,6 +87,7 @@ export const ROLE_POLICIES = [
'canManageAvatarDecorations',
'canSearchNotes',
'canUseTranslator',
'canUseTTS',
'canHideAds',
'driveCapacityMb',
'alwaysMarkNsfw',

View file

@ -84,6 +84,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
<div v-if="converting || convert" :class="$style.translation">
<MkLoading v-if="converting" mini/>
<div v-else-if="converturl">
<!--<b>{{ i18n.tsx.convertedFrom({ x: appearNote.id }) }}: </b>-->
<b>{{ 'From ' + appearNote.id }} </b>
<MkMediaAudio :audio="converturl"/>
</div>
</div>
</div>
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
@ -167,7 +175,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide, onUnmounted } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
@ -177,6 +185,7 @@ import type { MenuItem } from '@/types/menu.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkMediaAudio from '@/components/MkMediaAudio.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
import MkMediaList from '@/components/MkMediaList.vue';
@ -272,6 +281,9 @@ const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const convert = ref<Blob | null>(null);
const converting = ref(false);
const converturl = ref<Misskey.entities.NotesTTSResponse | null>(null);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const renoteCollapsed = ref(
@ -531,7 +543,7 @@ function showMenu(): void {
return;
}
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, convert, converting, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}
@ -609,6 +621,24 @@ function emitUpdReaction(emoji: string, delta: number) {
emit('reaction', emoji);
}
}
watch(convert, (newBlob) => {
try {
if (newBlob) {
converturl.value = { url: newBlob };
} else {
converturl.value = null;
}
} catch (error) {
console.error('Failed to create URL:', error);
}
});
onUnmounted(() => {
if (converturl.value && converturl.value.url) {
URL.revokeObjectURL(converturl.value.url);
}
});
</script>
<style lang="scss" module>

View file

@ -99,6 +99,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
<div v-if="converting || convert" :class="$style.translation">
<MkLoading v-if="converting" mini/>
<div v-else-if="converturl">
<!--<b>{{ i18n.tsx.convertedFrom({ x: appearNote.id }) }}: </b>-->
<b>{{ 'From ' + appearNote.id }} </b>
<MkMediaAudio :audio="converturl"/>
</div>
</div>
<div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
</div>
@ -203,7 +211,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
import { computed, inject, onMounted, provide, ref, shallowRef, watch, onUnmounted } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
@ -211,6 +219,7 @@ import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
import MkMediaAudio from '@/components/MkMediaAudio.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkCwButton from '@/components/MkCwButton.vue';
import MkPoll from '@/components/MkPoll.vue';
@ -292,6 +301,9 @@ const isDeleted = ref(false);
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const convert = ref<Blob | null>(null);
const converting = ref(false);
const converturl = ref<Misskey.entities.NotesTTSResponse | null>(null);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
@ -483,13 +495,13 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, converting, convert, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function showMenu(): void {
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, converting, convert, isDeleted });
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}
@ -544,6 +556,24 @@ function loadConversation() {
conversation.value = res.reverse();
});
}
watch(convert, (newBlob) => {
try {
if (newBlob) {
converturl.value = { url: newBlob };
} else {
converturl.value = null;
}
} catch (error) {
console.error('Failed to create URL:', error);
}
});
onUnmounted(() => {
if (converturl.value && converturl.value.url) {
URL.revokeObjectURL(converturl.value.url);
}
});
</script>
<style lang="scss" module>

View file

@ -52,6 +52,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB',
isBot: false,
isCat: true,
isVI: false,
emojis: {},
onlineStatus: 'unknown',
badgeRoles: [],

View file

@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
<option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
<option value="isCat">{{ i18n.ts._role._condition.isCat }}</option>
<option value="isVI">{{ i18n.ts._role._condition.isVI }}</option>
<option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option>
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>

View file

@ -5,25 +5,99 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<MkFolder>
<template #label>DeepL Translation</template>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<MkFolder>
<template #label>DeepL Translation</template>
<div class="_gaps_m">
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
</FormSuspense>
</MkSpacer>
<div class="_gaps_m">
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
<br />
<MkFolder>
<template #label>Text-To-Speech</template>
<div class="_gaps_m">
<MkInput v-model="hfAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>HuggingFace Auth Key</template>
</MkInput>
<MkSwitch v-model="hfSpace">
<template #label>HuggingFace Space</template>
</MkSwitch>
<div v-if="hfSpace">
<MkInput v-model="hfSpaceName">
<template #label>Space Name</template>
</MkInput>
<MkInput v-model="hfexampleAudioURL">
<template #label>Example Audio URL</template>
</MkInput>
<br />
<MkSwitch v-model="hfnrm">
<template #label>Enable no reference mode</template>
</MkSwitch>
<br />
<div v-if="!hfnrm">
<MkInput v-model="hfexampleText">
<template #label>Example Text</template>
</MkInput>
</div>
<MkSelect v-model="hfexampleLang">
<template #label>Example Language</template>
<option value="" disabled> </option>
<option value="Chinese">中文</option>
<option value="English">English</option>
<option value="Japanese">日本語</option>
<option value="Yue">中文 (粤语)</option>
<option value="Korean">한국어</option>
<option value="Chinese-English Mixed">中文 - English</option>
<option value="Japanese-English Mixed">日本語 - English</option>
<option value="Yue-English Mixed">中文 (粤语) - English</option>
<option value="Korean-English Mixed">한국어 - English</option>
<option value="Multilingual Mixed">Multilingual Mixed</option>
<option value="Multilingual Mixed(Yue)">Multilingual Mixed (Yue)</option>
</MkSelect>
<br />
<MkSwitch v-model="hfdas">
<template #label>Whether to directly adjust the speech rate and timebre of the last synthesis result to prevent randomness</template>
</MkSwitch>
<br />
<MkSelect v-model="hfslice">
<template #label>Slice</template>
<option value="" disabled> </option>
<option value="No slice">No slice</option>
<option value="Slice once every 4 sentences">Slice once every 4 sentences</option>
<option value="Slice per 50 characters">Slice per 50 characters</option>
<option value="Slice by Chinese punct">Slice by Chinese punct</option>
<option value="Slice by English punct">Slice by English punct</option>
<option value="Slice by every punct">Slice by every punct</option>
</MkSelect>
<MkInput v-model.number="hftopK" type="range" :min="0" :max="100" :step="1">
<template #label>Set top_k Value: {{ hftopK }}</template>
</MkInput>
<MkInput v-model.number="hftopP" type="range" :min="0" :max="100" :step="5">
<template #label>Set top_p Value: {{ hftopP }}</template>
</MkInput>
<MkInput v-model.number="hfTemperature" type="range" :min="0" :max="100" :step="5">
<template #label>Set Temperature Value: {{ hfTemperature }}</template>
</MkInput>
<MkInput v-model.number="hfSpeedRate" type="range" :min="60" :max="165" :step="5">
<template #label>Set Speed Rate Value: {{ hfSpeedRate }}</template>
</MkInput>
</div>
<MkButton primary @click="save_tts">Save</MkButton>
</div>
</MkFolder>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
@ -32,6 +106,7 @@ import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
@ -43,20 +118,66 @@ import MkFolder from '@/components/MkFolder.vue';
const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
const hfAuthKey = ref<string>('');
const hfSpace = ref<boolean>(false);
const hfSpaceName = ref<string | null>(null);
const hfexampleAudioURL = ref<string | null>(null);
const hfexampleText = ref<string | null>(null);
const hfexampleLang = ref<string | null>(null);
const hfslice = ref<string | null>('Slice once every 4 sentences');
const hftopK = ref<number>(15);
const hftopP = ref<number>(100);
const hfTemperature = ref<number>(100);
const hfnrm = ref<boolean>(false);
const hfSpeedRate = ref<number>(125);
const hfdas = ref<boolean>(false);
async function init() {
const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey;
deeplIsPro.value = meta.deeplIsPro;
const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey;
deeplIsPro.value = meta.deeplIsPro;
hfAuthKey.value = meta.hfAuthKey;
hfSpace.value = meta.hfSpace;
hfSpaceName.value = meta.hfSpaceName;
hfexampleAudioURL.value = meta.hfexampleAudioURL;
hfexampleText.value = meta.hfexampleText;
hfexampleLang.value = meta.hfexampleLang;
hfslice.value = meta.hfslice;
hftopK.value = meta.hftopK;
hftopP.value = meta.hftopP;
hfTemperature.value = meta.hfTemperature;
hfnrm.value = meta.hfnrm;
hfSpeedRate.value = meta.hfSpeedRate;
hfdas.value = meta.hfdas;
}
function save_deepl() {
os.apiWithDialog('admin/update-meta', {
deeplAuthKey: deeplAuthKey.value,
deeplIsPro: deeplIsPro.value,
}).then(() => {
fetchInstance(true);
});
os.apiWithDialog('admin/update-meta', {
deeplAuthKey: deeplAuthKey.value,
deeplIsPro: deeplIsPro.value,
}).then(() => {
fetchInstance(true);
});
}
function save_tts() {
os.apiWithDialog('admin/update-meta', {
hfAuthKey: hfAuthKey.value,
hfSpace: hfSpace.value,
hfSpaceName: hfSpaceName.value,
hfexampleAudioURL: hfexampleAudioURL.value,
hfexampleText: hfexampleText.value,
hfexampleLang: hfexampleLang.value,
hfslice: hfslice.value,
hftopK: hftopK.value,
hftopP: hftopP.value,
hfTemperature: hfTemperature.value,
hfnrm: hfnrm.value,
hfSpeedRate: hfSpeedRate.value,
hfdas: hfdas.value,
}).then(() => {
fetchInstance(true);
});
}
const headerActions = computed(() => []);
@ -64,7 +185,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({
title: i18n.ts.externalServices,
icon: 'ti ti-link',
title: i18n.ts.externalServices,
icon: 'ti ti-link',
}));
</script>

View file

@ -338,6 +338,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTTS, 'canUseTTS'])">
<template #label>{{ i18n.ts._role._options.canUseTTS }}</template>
<template #suffix>
<span v-if="role.policies.canUseTTS.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUseTTS.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseTTS)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUseTTS.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUseTTS.value" :disabled="role.policies.canUseTTS.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUseTTS.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<template #suffix>

View file

@ -121,6 +121,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTTS, 'canSearchNotes'])">
<template #label>{{ i18n.ts._role._options.canUseTTS }}</template>
<template #suffix>{{ policies.canUseTTS ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUseTTS">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<template #suffix>{{ policies.driveCapacityMb }}MB</template>

View file

@ -110,6 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
<MkSwitch v-model="profile.isVI">{{ i18n.ts.flagAsVI }}<template #caption>{{ i18n.ts.flagAsVIDescription }}</template></MkSwitch>
</div>
</MkFolder>
</div>
@ -155,6 +156,7 @@ const profile = reactive({
lang: assertVaildLang($i.lang) ? $i.lang : null,
isBot: $i.isBot ?? false,
isCat: $i.isCat ?? false,
isVI: $i.isVI ?? false,
});
watch(() => profile, () => {
@ -206,6 +208,7 @@ function save() {
lang: profile.lang || null,
isBot: !!profile.isBot,
isCat: !!profile.isCat,
isVI: !!profile.isVI,
}, undefined, {
'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
title: i18n.ts.yourNameContainsProhibitedWords,

View file

@ -176,6 +176,8 @@ export function getNoteMenu(props: {
note: Misskey.entities.Note;
translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
translating: Ref<boolean>;
convert: Ref<string | null>;
converting: Ref<boolean>;
isDeleted: Ref<boolean>;
currentClip?: Misskey.entities.Clip;
}) {
@ -294,6 +296,38 @@ export function getNoteMenu(props: {
props.translation.value = res;
}
async function convert(): Promise<void> {
if (props.convert.value != null) return;
props.converting.value = true;
const res = await misskeyApi('notes/tts', {
noteId: appearNote.id,
}, undefined, undefined, true);
try {
if (res.body instanceof ReadableStream) {
const reader = res.body.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const audioBlob = new Blob(chunks, { type: 'audio/flac' });
props.convert.value = URL.createObjectURL(audioBlob);
} else {
console.error('Response body is not a ReadableStream');
}
} catch (errors) {
console.error('Failed to create Blob or Object URL:', e);
}
props.converting.value = false;
}
const menuItems: MenuItem[] = [];
if ($i) {
@ -348,6 +382,14 @@ export function getNoteMenu(props: {
});
}
if ($i.policies.canUseTTS && instance.ttsAvailable) {
menuItems.push({
icon: 'ti ti-headphones',
text: 'TTS',
action: convert,
});
}
menuItems.push({ type: 'divider' });
menuItems.push(statePromise.then(state => state.isFavorited ? {

View file

@ -20,7 +20,8 @@ export function misskeyApi<
data: P = {} as any,
token?: string | null | undefined,
signal?: AbortSignal,
): Promise<_ResT> {
returnResponse: boolean = false
): Promise<_ResT | Response> {
if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++;
@ -28,7 +29,7 @@ export function misskeyApi<
pendingApiRequestsCount.value--;
};
const promise = new Promise<_ResT>((resolve, reject) => {
const promise = new Promise<_ResT | Response>((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
@ -44,14 +45,17 @@ export function misskeyApi<
},
signal,
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
if (returnResponse) {
resolve(res);
} else {
reject(body.error);
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
} else {
reject(body.error);
}
}
}).catch(reject);
});
@ -70,7 +74,8 @@ export function misskeyApiGet<
>(
endpoint: E,
data: P = {} as any,
): Promise<_ResT> {
returnResponse: boolean = false
): Promise<_ResT | Response> {
pendingApiRequestsCount.value++;
const onFinally = () => {
@ -79,21 +84,24 @@ export function misskeyApiGet<
const query = new URLSearchParams(data as any);
const promise = new Promise<_ResT>((resolve, reject) => {
const promise = new Promise<_ResT | Response>((resolve, reject) => {
// Send request
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET',
credentials: 'omit',
cache: 'default',
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
if (returnResponse) {
resolve(res);
} else {
reject(body.error);
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
} else {
reject(body.error);
}
}
}).catch(reject);
});
@ -102,3 +110,4 @@ export function misskeyApiGet<
return promise;
}

View file

@ -1671,6 +1671,8 @@ declare namespace entities {
NotesTimelineResponse,
NotesTranslateRequest,
NotesTranslateResponse,
NotesTtsRequest,
NotesTtsResponse,
NotesUnrenoteRequest,
NotesUserListTimelineRequest,
NotesUserListTimelineResponse,
@ -2785,6 +2787,12 @@ type NotesTranslateRequest = operations['notes___translate']['requestBody']['con
// @public (undocumented)
type NotesTranslateResponse = operations['notes___translate']['responses']['200']['content']['application/json'];
// @public (undocumented)
type NotesTtsRequest = operations['notes___tts']['requestBody']['content']['application/json'];
// @public (undocumented)
type NotesTtsResponse = operations['notes___tts']['responses']['200']['content']['application/json'];
// @public (undocumented)
type NotesUnrenoteRequest = operations['notes___unrenote']['requestBody']['content']['application/json'];

View file

@ -3339,6 +3339,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
request<E extends 'notes/tts', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View file

@ -450,6 +450,8 @@ import type {
NotesTimelineResponse,
NotesTranslateRequest,
NotesTranslateResponse,
NotesTtsRequest,
NotesTtsResponse,
NotesUnrenoteRequest,
NotesUserListTimelineRequest,
NotesUserListTimelineResponse,
@ -878,6 +880,7 @@ export type Endpoints = {
'notes/thread-muting/delete': { req: NotesThreadMutingDeleteRequest; res: EmptyResponse };
'notes/timeline': { req: NotesTimelineRequest; res: NotesTimelineResponse };
'notes/translate': { req: NotesTranslateRequest; res: NotesTranslateResponse };
'notes/tts': { req: NotesTtsRequest; res: NotesTtsResponse };
'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse };
'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse };
'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse };

View file

@ -453,6 +453,8 @@ export type NotesTimelineRequest = operations['notes___timeline']['requestBody']
export type NotesTimelineResponse = operations['notes___timeline']['responses']['200']['content']['application/json'];
export type NotesTranslateRequest = operations['notes___translate']['requestBody']['content']['application/json'];
export type NotesTranslateResponse = operations['notes___translate']['responses']['200']['content']['application/json'];
export type NotesTtsRequest = operations['notes___tts']['requestBody']['content']['application/json'];
export type NotesTtsResponse = operations['notes___tts']['responses']['200']['content']['application/json'];
export type NotesUnrenoteRequest = operations['notes___unrenote']['requestBody']['content']['application/json'];
export type NotesUserListTimelineRequest = operations['notes___user-list-timeline']['requestBody']['content']['application/json'];
export type NotesUserListTimelineResponse = operations['notes___user-list-timeline']['responses']['200']['content']['application/json'];

View file

@ -2890,6 +2890,15 @@ export type paths = {
*/
post: operations['notes___translate'];
};
'/notes/tts': {
/**
* notes/tts
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
post: operations['notes___tts'];
};
'/notes/unrenote': {
/**
* notes/unrenote
@ -3736,6 +3745,7 @@ export type components = {
}[];
isBot?: boolean;
isCat?: boolean;
isVI?: boolean;
instance?: {
name: string | null;
softwareName: string | null;
@ -4768,7 +4778,7 @@ export type components = {
RoleCondFormulaValueUserSettingBooleanSchema: {
id: string;
/** @enum {string} */
type: 'isSuspended' | 'isLocked' | 'isBot' | 'isCat' | 'isExplorable';
type: 'isSuspended' | 'isLocked' | 'isBot' | 'isCat' | 'isVI' | 'isExplorable';
};
RoleCondFormulaValueAssignedRole: {
id: string;
@ -4850,6 +4860,7 @@ export type components = {
canManageAvatarDecorations: boolean;
canSearchNotes: boolean;
canUseTranslator: boolean;
canUseTTS: boolean;
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
@ -5001,6 +5012,7 @@ export type components = {
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
ttsAvailable: boolean;
mediaProxy: string;
enableUrlPreview: boolean;
backgroundImageUrl: string | null;
@ -5117,6 +5129,7 @@ export type operations = {
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
ttsAvailable: boolean;
silencedHosts?: string[];
mediaSilencedHosts: string[];
pinnedUsers: string[];
@ -5181,6 +5194,19 @@ export type operations = {
backgroundImageUrl: string | null;
deeplAuthKey: string | null;
deeplIsPro: boolean;
hfAuthKey: string | null;
hfSpace: boolean;
hfSpaceName: string | null;
hfexampleAudioURL: string | null;
hfexampleText: string | null;
hfexampleLang: string | null;
hfslice: string | null;
hftopK: number | null;
hftopP: number | null;
hfTemperature: number | null;
hfSpeedRate: number | null;
hfnrm: boolean;
hfdas: boolean;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
description: string | null;
@ -9510,6 +9536,27 @@ export type operations = {
langs?: string[];
deeplAuthKey?: string | null;
deeplIsPro?: boolean;
hfAuthKey?: string | null;
/** @default false */
hfSpace?: boolean;
hfSpaceName?: string | null;
hfexampleAudioURL?: string | null;
hfexampleText?: string | null;
hfexampleLang?: string | null;
/** @default Slice once every 4 sentences */
hfslice?: string | null;
/** @default 15 */
hftopK?: number;
/** @default 100 */
hftopP?: number;
/** @default 100 */
hfTemperature?: number;
/** @default false */
hfnrm?: boolean;
/** @default 125 */
hfSpeedRate?: number;
/** @default false */
hfdas?: boolean;
enableEmail?: boolean;
email?: string | null;
smtpSecure?: boolean;
@ -19846,6 +19893,7 @@ export type operations = {
preventAiLearning?: boolean;
isBot?: boolean;
isCat?: boolean;
isVI?: boolean;
injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;
@ -23007,6 +23055,64 @@ export type operations = {
};
};
};
/**
* notes/tts
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
notes___tts: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
noteId: string;
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': string;
};
};
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* notes/unrenote
* @description No description provided.

135
pnpm-lock.yaml generated
View file

@ -125,6 +125,9 @@ importers:
'@fastify/view':
specifier: 10.0.1
version: 10.0.1
'@gradio/client':
specifier: 1.6.0-beta.3
version: 1.6.0-beta.3(utf-8-validate@6.0.3)
'@misskey-dev/sharp-read-bmp':
specifier: 1.2.0
version: 1.2.0
@ -1163,7 +1166,7 @@ importers:
version: 7.17.0(eslint@9.11.0)(typescript@5.6.2)
'@vitest/coverage-v8':
specifier: 1.6.0
version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0))
version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))
'@vue/runtime-core':
specifier: 3.5.11
version: 3.5.11
@ -2772,6 +2775,10 @@ packages:
resolution: {integrity: sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==}
hasBin: true
'@gradio/client@1.6.0-beta.3':
resolution: {integrity: sha512-mJZVQ4UpfrSu71J4SkbSrpnbRotmB5ziy4fg7zqZhqXwXGZM3cHR9fUGkTFM0eXYnsaeBoiqn+1bcUh32Zcgkg==}
engines: {node: '>=18.0.0'}
'@hapi/boom@10.0.1':
resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==}
@ -4633,6 +4640,9 @@ packages:
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@types/eventsource@1.1.15':
resolution: {integrity: sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==}
'@types/express-serve-static-core@4.17.33':
resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
@ -6833,6 +6843,10 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
eventsource@2.0.2:
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
engines: {node: '>=12.0.0'}
execa@0.7.0:
resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==}
engines: {node: '>=4'}
@ -6975,6 +6989,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fetch-event-stream@0.1.5:
resolution: {integrity: sha512-V1PWovkspxQfssq/NnxoEyQo1DV+MRK/laPuPblIZmSjMN8P5u46OhlFQznSr9p/t0Sp8Uc6SbM3yCMfr0KU8g==}
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@ -10236,6 +10253,10 @@ packages:
seedrandom@3.0.5:
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
semiver@1.1.0:
resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==}
engines: {node: '>=6'}
semver-regex@4.0.5:
resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==}
engines: {node: '>=12'}
@ -10806,6 +10827,9 @@ packages:
textarea-caret@3.1.0:
resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==}
textlinestream@1.1.1:
resolution: {integrity: sha512-iBHbi7BQxrFmwZUQJsT0SjNzlLLsXhvW/kg7EyOMVMBIrlnj/qYofwo1LVLZi+3GbUEo96Iu2eqToI2+lZoAEQ==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@ -13245,6 +13269,20 @@ snapshots:
'@github/webauthn-json@2.1.1': {}
'@gradio/client@1.6.0-beta.3(utf-8-validate@6.0.3)':
dependencies:
'@types/eventsource': 1.1.15
bufferutil: 4.0.7
eventsource: 2.0.2
fetch-event-stream: 0.1.5
msw: 2.4.9(typescript@5.6.2)
semiver: 1.1.0
textlinestream: 1.1.1
typescript: 5.6.2
ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
transitivePeerDependencies:
- utf-8-validate
'@hapi/boom@10.0.1':
dependencies:
'@hapi/hoek': 11.0.4
@ -15573,6 +15611,8 @@ snapshots:
'@types/estree@1.0.6': {}
'@types/eventsource@1.1.15': {}
'@types/express-serve-static-core@4.17.33':
dependencies:
'@types/node': 20.14.12
@ -15962,7 +16002,7 @@ snapshots:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
'@typescript-eslint/visitor-keys': 7.17.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.11.0
optionalDependencies:
typescript: 5.5.4
@ -15975,7 +16015,7 @@ snapshots:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
'@typescript-eslint/visitor-keys': 7.17.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.11.0
optionalDependencies:
typescript: 5.6.2
@ -15988,7 +16028,7 @@ snapshots:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
'@typescript-eslint/visitor-keys': 7.17.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
optionalDependencies:
typescript: 5.6.2
@ -16009,7 +16049,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
'@typescript-eslint/utils': 7.1.0(eslint@9.11.0)(typescript@5.3.3)
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.11.0
ts-api-utils: 1.0.1(typescript@5.3.3)
optionalDependencies:
@ -16021,7 +16061,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
'@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.5.4)
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.11.0
ts-api-utils: 1.3.0(typescript@5.5.4)
optionalDependencies:
@ -16033,7 +16073,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
'@typescript-eslint/utils': 7.17.0(eslint@9.11.0)(typescript@5.6.2)
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.11.0
ts-api-utils: 1.3.0(typescript@5.6.2)
optionalDependencies:
@ -16045,7 +16085,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2)
'@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
ts-api-utils: 1.3.0(typescript@5.6.2)
optionalDependencies:
@ -16061,7 +16101,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/visitor-keys': 7.1.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
@ -16076,7 +16116,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/visitor-keys': 7.17.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
@ -16091,7 +16131,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/visitor-keys': 7.17.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
@ -16175,7 +16215,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.4
@ -16190,11 +16230,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0))':
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))':
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.4
@ -16205,7 +16245,7 @@ snapshots:
std-env: 3.7.0
strip-literal: 2.1.0
test-exclude: 6.0.0
vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0)
vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0)
transitivePeerDependencies:
- supports-color
@ -16515,7 +16555,7 @@ snapshots:
agent-base@7.1.0:
dependencies:
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -17001,7 +17041,6 @@ snapshots:
bufferutil@4.0.7:
dependencies:
node-gyp-build: 4.6.0
optional: true
bufferutil@4.0.8:
dependencies:
@ -18638,6 +18677,8 @@ snapshots:
events@3.3.0: {}
eventsource@2.0.2: {}
execa@0.7.0:
dependencies:
cross-spawn: 5.1.0
@ -18885,6 +18926,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
fetch-event-stream@0.1.5: {}
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@ -19487,7 +19530,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -19526,7 +19569,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
optional: true
@ -19534,14 +19577,14 @@ snapshots:
https-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.0
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@ -19914,7 +19957,7 @@ snapshots:
istanbul-lib-source-maps@5.0.4:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@ -20315,6 +20358,35 @@ snapshots:
jsdoc-type-pratt-parser@4.1.0: {}
jsdom@24.1.1:
dependencies:
cssstyle: 4.0.1
data-urls: 5.0.0
decimal.js: 10.4.3
form-data: 4.0.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.12
parse5: 7.1.2
rrweb-cssom: 0.7.1
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 4.1.4
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
optional: true
jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
dependencies:
cssstyle: 4.0.1
@ -21361,8 +21433,7 @@ snapshots:
node-gyp-build-optional-packages@5.0.7:
optional: true
node-gyp-build@4.6.0:
optional: true
node-gyp-build@4.6.0: {}
node-gyp@10.2.0:
dependencies:
@ -22741,6 +22812,8 @@ snapshots:
seedrandom@3.0.5: {}
semiver@1.1.0: {}
semver-regex@4.0.5: {}
semver-truncate@2.0.0:
@ -22889,7 +22962,7 @@ snapshots:
dependencies:
'@hapi/hoek': 11.0.4
'@hapi/wreck': 18.0.1
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
joi: 17.11.0
transitivePeerDependencies:
- supports-color
@ -23376,6 +23449,8 @@ snapshots:
textarea-caret@3.1.0: {}
textlinestream@1.1.1: {}
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@ -23849,7 +23924,7 @@ snapshots:
vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0):
dependencies:
cac: 6.7.14
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
@ -23867,7 +23942,7 @@ snapshots:
vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
dependencies:
cac: 6.7.14
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
@ -23949,7 +24024,7 @@ snapshots:
- supports-color
- terser
vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.33.0):
vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0):
dependencies:
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
@ -23974,7 +24049,7 @@ snapshots:
optionalDependencies:
'@types/node': 20.14.12
happy-dom: 10.0.3
jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
jsdom: 24.1.1
transitivePeerDependencies:
- less
- lightningcss
@ -24046,7 +24121,7 @@ snapshots:
vue-eslint-parser@9.4.3(eslint@9.11.0):
dependencies:
debug: 4.3.5(supports-color@5.5.0)
debug: 4.3.5(supports-color@8.1.1)
eslint: 9.11.0
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3