Add eslint, prettier and update node-version
This commit is contained in:
parent
ebb786b3ed
commit
fcefc5a41f
50 changed files with 4575 additions and 1851 deletions
|
|
@ -1 +1 @@
|
||||||
v24.13.0
|
v26.2.0
|
||||||
|
|
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
14
.vscode/settings.json
vendored
Normal file
14
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ use std::{collections::HashMap, path::PathBuf};
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod tts;
|
pub mod tts;
|
||||||
|
|
||||||
use tts::{TtsOpts, TtsPool, load_text_to_speech, load_voice_style};
|
use tts::{TtsOpts, TtsPool, load_text_to_speech};
|
||||||
|
|
||||||
use crate::tts::engine::load_voice_style_map;
|
use crate::tts::engine::load_voice_style_map;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
std::env::var("SUPERTONIC_MODEL_DIR").unwrap_or_else(|_| "./assets".to_string());
|
std::env::var("SUPERTONIC_MODEL_DIR").unwrap_or_else(|_| "./assets".to_string());
|
||||||
let voice_style_path = std::env::var("SUPERTONIC_VOICE_STYLE")
|
let voice_style_path = std::env::var("SUPERTONIC_VOICE_STYLE")
|
||||||
.unwrap_or_else(|_| format!("F1={model_dir}/voice_styles/F1.json"));
|
.unwrap_or_else(|_| format!("F1={model_dir}/voice_styles/F1.json"));
|
||||||
let lang = std::env::var("SUPERTONIC_LANG").unwrap_or_else(|_| "en".to_string());
|
let _lang = std::env::var("SUPERTONIC_LANG").unwrap_or_else(|_| "en".to_string());
|
||||||
let total_step: usize = std::env::var("SUPERTONIC_TOTAL_STEP")
|
let total_step: usize = std::env::var("SUPERTONIC_TOTAL_STEP")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
extends: [js.configs.recommended, tseslint.configs.recommended],
|
||||||
|
basePath: "packages",
|
||||||
|
files: ["**/*.ts"],
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: "off", // Or use false / 0
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-namespace": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
},
|
||||||
|
});
|
||||||
1289
package-lock.json
generated
1289
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
|
@ -1,15 +1,19 @@
|
||||||
{
|
{
|
||||||
"main": "dist",
|
"main": "dist",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prisma:push": "prisma db push",
|
"prisma:push": "prisma db push",
|
||||||
"format:prisma": "prisma format",
|
"prisma:format": "prisma format",
|
||||||
"format": "npm run format:prisma",
|
"check": "npm run check:tsc && npm run check:eslint && npm run check:format",
|
||||||
|
"check:eslint": "eslint",
|
||||||
|
"check:tsc": "./node_modules/typescript/bin/tsc --noEmit",
|
||||||
|
"check:format": "prettier --check .",
|
||||||
|
"fix:format": "prettier --write .",
|
||||||
"build:prisma": "prisma generate",
|
"build:prisma": "prisma generate",
|
||||||
"build:tsc": "tsc",
|
"build:tsc": "./node_modules/typescript/bin/tsc",
|
||||||
"build": "npm run build:prisma && npm run build:tsc",
|
"build": "npm run build:prisma && npm run build:tsc",
|
||||||
"start": "prisma migrate deploy && node .",
|
"start": "prisma migrate deploy && node --import=extensionless/register .",
|
||||||
"start:no-db": "npm run build:tsc && node .",
|
"dev": "npm run build && node --import=extensionless/register ."
|
||||||
"dev": "npm run build && npm run start"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.10.0",
|
"@discordjs/opus": "^0.10.0",
|
||||||
|
|
@ -19,18 +23,23 @@
|
||||||
"@snazzah/davey": "^0.1.9",
|
"@snazzah/davey": "^0.1.9",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"extensionless": "^2.0.6",
|
||||||
"fastify": "^5.7.1",
|
"fastify": "^5.7.1",
|
||||||
"libsodium-wrappers": "^0.8.0",
|
"libsodium-wrappers": "^0.8.0",
|
||||||
"opusscript": "^0.0.8",
|
"opusscript": "^0.0.8",
|
||||||
"pg": "^8.17.1",
|
"pg": "^8.17.1",
|
||||||
"play-dl": "^1.9.7",
|
"play-dl": "^1.9.7",
|
||||||
"prism-media": "^1.3.5",
|
"prism-media": "^1.3.5",
|
||||||
"prisma": "^7.2.0",
|
"prisma": "^7.2.0"
|
||||||
"typescript": "^5.9.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/ffprobe-static": "^2.0.3",
|
"@types/ffprobe-static": "^2.0.3",
|
||||||
"@types/node": "25.0.9",
|
"@types/node": "25.0.9",
|
||||||
"@types/pg": "^8.16.0"
|
"@types/pg": "^8.16.0",
|
||||||
|
"eslint": "^10.4.0",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
|
"typescript-eslint": "^8.59.4",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { setUserCanTypecast } from "./db";
|
import { setUserCanTypecast } from "./db";
|
||||||
|
|
||||||
export const AdminUsers = [ "858173387775148073", "367946917197381644" ];
|
export const AdminUsers = ["858173387775148073", "367946917197381644"];
|
||||||
|
|
||||||
AdminUsers.forEach(userid => {
|
AdminUsers.forEach((userid) => {
|
||||||
setUserCanTypecast(userid, true);
|
setUserCanTypecast(userid, true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,56 @@
|
||||||
import { ChatInputCommandInteraction, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandBuilder } from "discord.js";
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
type SlashCommandOptionsOnlyBuilder,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { requireDirectorySync } from "../utils/requireDirectory";
|
import { requireDirectory } from "../utils/requireDirectory";
|
||||||
import { AdminUsers } from "./admin";
|
import { AdminUsers } from "./admin";
|
||||||
|
|
||||||
export type DiscordCommandData = SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandBuilder
|
export type DiscordCommandData =
|
||||||
export type DiscordCommandExecute = (interaction: ChatInputCommandInteraction) => Promise<any>
|
| SlashCommandBuilder
|
||||||
|
| SlashCommandOptionsOnlyBuilder
|
||||||
|
| SlashCommandSubcommandBuilder;
|
||||||
|
export type DiscordCommandExecute = (
|
||||||
|
interaction: ChatInputCommandInteraction,
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
export interface DiscordCommand {
|
export interface DiscordCommand {
|
||||||
data: DiscordCommandData
|
data: DiscordCommandData;
|
||||||
execute: DiscordCommandExecute
|
execute: DiscordCommandExecute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineCommand(
|
export function defineCommand(
|
||||||
data: DiscordCommandData,
|
data: DiscordCommandData,
|
||||||
execute: DiscordCommandExecute,
|
execute: DiscordCommandExecute,
|
||||||
isAdminCommand=false,
|
isAdminCommand = false,
|
||||||
): DiscordCommand {
|
): DiscordCommand {
|
||||||
if (isAdminCommand) {
|
if (isAdminCommand) {
|
||||||
return {
|
|
||||||
data: data,
|
|
||||||
execute: async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
|
||||||
if (AdminUsers.includes(interaction.user.id)) {
|
|
||||||
execute(interaction);
|
|
||||||
} else {
|
|
||||||
interaction.reply("당신은 어드민이 아닙니다");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
data: data,
|
data: data,
|
||||||
execute: execute,
|
execute: async (
|
||||||
}
|
interaction: ChatInputCommandInteraction,
|
||||||
|
): Promise<any> => {
|
||||||
|
if (AdminUsers.includes(interaction.user.id)) {
|
||||||
|
execute(interaction);
|
||||||
|
} else {
|
||||||
|
interaction.reply("당신은 어드민이 아닙니다");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
execute: execute,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandDirectory = join(__dirname, "commands");
|
const commandDirectory = join(import.meta.dirname, "commands");
|
||||||
export const defineCommands = requireDirectorySync<DiscordCommand>(commandDirectory);
|
export const defineCommands =
|
||||||
|
await requireDirectory<DiscordCommand>(commandDirectory);
|
||||||
export const commandExecuteNameHashMap: {
|
export const commandExecuteNameHashMap: {
|
||||||
[key: string]: DiscordCommandExecute
|
[key: string]: DiscordCommandExecute;
|
||||||
} = Object.fromEntries(defineCommands.map(command => [command.data.name, command.execute]));
|
} = Object.fromEntries(
|
||||||
|
defineCommands.map((command) => [command.data.name, command.execute]),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import {
|
||||||
import { defineCommand } from "../command";
|
ChatInputCommandInteraction,
|
||||||
import { getGuildProfile } from "../db";
|
MessageFlags,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
|
import { defineCommand } from "../command.js";
|
||||||
|
import { getGuildProfile } from "../db.js";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName("읽는채널")
|
.setName("읽는채널")
|
||||||
.setDescription("예주가 읽어주는 채널들을 말해줘요"),
|
.setDescription("예주가 읽어주는 채널들을 말해줘요"),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
|
||||||
if (guildId == null)
|
if (guildId == null)
|
||||||
return await interaction.editReply("알수없는 서버에요!");
|
return await interaction.editReply("알수없는 서버에요!");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const guildProfile = await getGuildProfile(guildId);
|
const guildProfile = await getGuildProfile(guildId);
|
||||||
const readChannel = guildProfile.readChannel;
|
const readChannel = guildProfile.readChannel;
|
||||||
|
|
||||||
await interaction.editReply(readChannel.map(channel => `<#${channel}>`).join("\n") || "아무 채널도 읽지 않아요!");
|
await interaction.editReply(
|
||||||
} catch {
|
readChannel.map((channel: any) => `<#${channel}>`).join("\n") ||
|
||||||
await interaction.editReply("서버 프로필을 가져오는데 문제가 생겼어요");
|
"아무 채널도 읽지 않아요!",
|
||||||
}
|
);
|
||||||
|
} catch {
|
||||||
|
await interaction.editReply("서버 프로필을 가져오는데 문제가 생겼어요");
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,26 @@
|
||||||
import { AttachmentBuilder, ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import {
|
||||||
|
AttachmentBuilder,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { OutputHandler } from "../../utils/outputHandler";
|
import { OutputHandler } from "../../utils/outputHandler";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName("상태")
|
.setName("상태")
|
||||||
.setDescription("예주의 상태를 확인해요"),
|
.setDescription("예주의 상태를 확인해요"),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
if (interaction.guild == null)
|
if (interaction.guild == null)
|
||||||
return interaction.reply("올바르지 않은 서버에요");
|
return interaction.reply("올바르지 않은 서버에요");
|
||||||
|
|
||||||
const buffer = Buffer.from(await OutputHandler.getErrorLog(), 'utf-8');
|
const buffer = Buffer.from(await OutputHandler.getErrorLog(), "utf-8");
|
||||||
const attachment = new AttachmentBuilder(buffer, { name: 'result.txt' });
|
const attachment = new AttachmentBuilder(buffer, { name: "result.txt" });
|
||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: "제 상태에요",
|
content: "제 상태에요",
|
||||||
files: [attachment]
|
files: [attachment],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,56 @@
|
||||||
import { Channel, ChannelType, ChatInputCommandInteraction, GuildMember, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import {
|
||||||
|
type Channel,
|
||||||
|
ChannelType,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
GuildMember,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { joinVoiceChannel } from "@discordjs/voice";
|
import { joinVoiceChannel } from "@discordjs/voice";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName("등장")
|
.setName("등장")
|
||||||
.setDescription("예주가 등장해요")
|
.setDescription("예주가 등장해요")
|
||||||
.addChannelOption(option =>
|
.addChannelOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("channel")
|
.setName("channel")
|
||||||
.setDescription("예주가 등장할 채널이에요")
|
.setDescription("예주가 등장할 채널이에요")
|
||||||
.addChannelTypes(ChannelType.GuildVoice)
|
.addChannelTypes(ChannelType.GuildVoice),
|
||||||
),
|
),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
const member = interaction.member as GuildMember | null;
|
const member = interaction.member as GuildMember | null;
|
||||||
if (member == null)
|
if (member == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (interaction.guild == null)
|
if (interaction.guild == null)
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "올바르지 않은 서버에요",
|
content: "올바르지 않은 서버에요",
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = interaction.options.getChannel("channel") as Channel || member.voice.channel;
|
const channel =
|
||||||
|
(interaction.options.getChannel("channel") as Channel) ||
|
||||||
|
member.voice.channel;
|
||||||
|
|
||||||
if (channel == null)
|
if (channel == null)
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "통화방에 들어가거나 채널을 지정해주세요!",
|
content: "통화방에 들어가거나 채널을 지정해주세요!",
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
joinVoiceChannel({
|
joinVoiceChannel({
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
guildId: interaction.guild.id,
|
guildId: interaction.guild.id,
|
||||||
adapterCreator: interaction.guild.voiceAdapterCreator,
|
adapterCreator: interaction.guild.voiceAdapterCreator,
|
||||||
selfDeaf: false,
|
selfDeaf: false,
|
||||||
selfMute: false,
|
selfMute: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await interaction.editReply("등장했어요!");
|
await interaction.editReply("등장했어요!");
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import {
|
||||||
import { defineCommand, DiscordCommand } from "../command";
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
|
import { defineCommand } from "../command";
|
||||||
import { getVoiceConnection } from "@discordjs/voice";
|
import { getVoiceConnection } from "@discordjs/voice";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder().setName("퇴장").setDescription("예주가 퇴장해요"),
|
||||||
.setName("퇴장")
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
.setDescription("예주가 퇴장해요"),
|
await interaction.deferReply({
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
flags: [MessageFlags.Ephemeral],
|
||||||
await interaction.deferReply({
|
});
|
||||||
flags: [MessageFlags.Ephemeral]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (interaction.guild == null)
|
if (interaction.guild == null)
|
||||||
return interaction.editReply("올바르지 않은 서버에요");
|
return interaction.editReply("올바르지 않은 서버에요");
|
||||||
|
|
||||||
const connection = getVoiceConnection(interaction.guild.id);
|
const connection = getVoiceConnection(interaction.guild.id);
|
||||||
if (!connection)
|
if (!connection)
|
||||||
return interaction.editReply("예주는 통화방에 존제하지 않아요");
|
return interaction.editReply("예주는 통화방에 존제하지 않아요");
|
||||||
|
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
|
|
||||||
await interaction.editReply("퇴장했어요!");
|
await interaction.editReply("퇴장했어요!");
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,42 @@
|
||||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandSubcommandBuilder } from "discord.js";
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { playVoice } from "../tts";
|
import { playVoice } from "../tts";
|
||||||
import { getUserProfile } from "../db";
|
import { getUserProfile } from "../db";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandSubcommandBuilder()
|
new SlashCommandSubcommandBuilder()
|
||||||
.setName("말")
|
.setName("말")
|
||||||
.setDescription("구구가가")
|
.setDescription("구구가가")
|
||||||
.addStringOption(option =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("content")
|
.setName("content")
|
||||||
.setDescription("예주가 말해준데요")
|
.setDescription("예주가 말해준데요")
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
),
|
),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (interaction.guild == null)
|
if (interaction.guild == null)
|
||||||
return await interaction.editReply("올바르지 않은 서버에요");
|
return await interaction.editReply("올바르지 않은 서버에요");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userProfile = await getUserProfile(interaction.user.id);
|
const userProfile = await getUserProfile(interaction.user.id);
|
||||||
await playVoice(
|
await playVoice(
|
||||||
interaction.guild,
|
interaction.guild,
|
||||||
userProfile,
|
userProfile,
|
||||||
userProfile.voice,
|
userProfile.voice,
|
||||||
interaction.options.getString("content") as string
|
interaction.options.getString("content") as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.editReply("말했어요!");
|
await interaction.editReply("말했어요!");
|
||||||
} catch {
|
} catch {
|
||||||
await interaction.editReply("오늘따라 말이 꼬이네요 ㅜ.ㅜ");
|
await interaction.editReply("오늘따라 말이 꼬이네요 ㅜ.ㅜ");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,42 @@
|
||||||
import { Channel, ChannelType, ChatInputCommandInteraction, MessageFlags, SlashCommandSubcommandBuilder } from "discord.js";
|
import {
|
||||||
import { defineCommand, DiscordCommand } from "../command";
|
type Channel,
|
||||||
|
ChannelType,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
|
import { defineCommand } from "../command";
|
||||||
import { insertGuildReadChannel } from "../db";
|
import { insertGuildReadChannel } from "../db";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandSubcommandBuilder()
|
new SlashCommandSubcommandBuilder()
|
||||||
.setName("읽어")
|
.setName("읽어")
|
||||||
.setDescription("예주가 해당 채널을 읽어줘요")
|
.setDescription("예주가 해당 채널을 읽어줘요")
|
||||||
.addChannelOption(option =>
|
.addChannelOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("channel")
|
.setName("channel")
|
||||||
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildVoice)
|
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildVoice)
|
||||||
.setDescription("예주가 읽을 채널이에요")
|
.setDescription("예주가 읽을 채널이에요")
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
),
|
),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = interaction.options.getChannel("channel") as Channel;
|
const channel = interaction.options.getChannel("channel") as Channel;
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
|
||||||
if (guildId == null)
|
if (guildId == null)
|
||||||
return await interaction.editReply("알수없는 서버에요!");
|
return await interaction.editReply("알수없는 서버에요!");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await insertGuildReadChannel(guildId, channel.id);
|
await insertGuildReadChannel(guildId, channel.id);
|
||||||
} catch {
|
} catch {
|
||||||
return await interaction.editReply("읽는대 너무 어려워요..");
|
return await interaction.editReply("읽는대 너무 어려워요..");
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.editReply("예주가 이제 이 채널을 읽어요!");
|
await interaction.editReply("예주가 이제 이 채널을 읽어요!");
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
import { ChatInputCommandInteraction, GuildMember, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { getUserProfile, setUserNya } from "../db";
|
import { getUserProfile, setUserNya } from "../db";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder().setName("냥").setDescription("???"),
|
||||||
.setName("냥")
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
.setDescription("???"),
|
await interaction.deferReply({
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
flags: [MessageFlags.Ephemeral],
|
||||||
await interaction.deferReply({
|
});
|
||||||
flags: [MessageFlags.Ephemeral]
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await getUserProfile(interaction.user.id);
|
const profile = await getUserProfile(interaction.user.id);
|
||||||
await setUserNya(interaction.user.id, !profile.nya);
|
await setUserNya(interaction.user.id, !profile.nya);
|
||||||
|
|
||||||
await interaction.editReply(profile.nya ? "냐앙..." : "냐앙!!");
|
await interaction.editReply(profile.nya ? "냐앙..." : "냐앙!!");
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,31 @@
|
||||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandSubcommandBuilder } from "discord.js";
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { setUserSupertonicStyle } from "../db";
|
import { setUserSupertonicStyle } from "../db";
|
||||||
import { SUPERTONIC_STYLE_LIST } from "../../env";
|
import { SUPERTONIC_STYLE_LIST } from "../../env";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandSubcommandBuilder()
|
new SlashCommandSubcommandBuilder()
|
||||||
.setName("슈퍼토닉목소리")
|
.setName("슈퍼토닉목소리")
|
||||||
.setDescription("예주의 슈퍼토닉 목소리 스타일을 설정해요")
|
.setDescription("예주의 슈퍼토닉 목소리 스타일을 설정해요")
|
||||||
.addStringOption(option =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("style")
|
.setName("style")
|
||||||
.setDescription("사용할수 있는 목소리들이에요")
|
.setDescription("사용할수 있는 목소리들이에요")
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.addChoices(
|
.addChoices(...SUPERTONIC_STYLE_LIST),
|
||||||
...SUPERTONIC_STYLE_LIST
|
),
|
||||||
)
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
),
|
await interaction.deferReply({
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
flags: [MessageFlags.Ephemeral],
|
||||||
await interaction.deferReply({
|
});
|
||||||
flags: [MessageFlags.Ephemeral]
|
|
||||||
});
|
|
||||||
|
|
||||||
const style = interaction.options.getString("style") as string;
|
const style = interaction.options.getString("style") as string;
|
||||||
await setUserSupertonicStyle(interaction.user.id, style);
|
await setUserSupertonicStyle(interaction.user.id, style);
|
||||||
|
|
||||||
await interaction.editReply("예주의 목소리 스타일을 변경했어요!");
|
await interaction.editReply("예주의 목소리 스타일을 변경했어요!");
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,35 @@
|
||||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandSubcommandBuilder } from "discord.js";
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { Voice } from "../../db/generated/prisma/enums";
|
import { Voice } from "../../db/generated/prisma/enums";
|
||||||
import { setUserVoice } from "../db";
|
import { setUserVoice } from "../db";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandSubcommandBuilder()
|
new SlashCommandSubcommandBuilder()
|
||||||
.setName("목소리")
|
.setName("목소리")
|
||||||
.setDescription("예주의 목소리를 설정해요")
|
.setDescription("예주의 목소리를 설정해요")
|
||||||
.addStringOption(option =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("voice")
|
.setName("voice")
|
||||||
.setDescription("사용할수 있는 목소리들이에요")
|
.setDescription("사용할수 있는 목소리들이에요")
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.addChoices(
|
.addChoices(
|
||||||
{ name: "TypeCast", value: "TypeCast" },
|
{ name: "TypeCast", value: "TypeCast" },
|
||||||
{ name: "Papago", value: "Papago" },
|
{ name: "Papago", value: "Papago" },
|
||||||
{ name: "Supertonic", value: "Supertonic" }
|
{ name: "Supertonic", value: "Supertonic" },
|
||||||
)
|
|
||||||
),
|
),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
),
|
||||||
await interaction.deferReply({
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
flags: [MessageFlags.Ephemeral]
|
await interaction.deferReply({
|
||||||
});
|
flags: [MessageFlags.Ephemeral],
|
||||||
|
});
|
||||||
|
|
||||||
const voice = interaction.options.getString("voice") as Voice;
|
const voice = interaction.options.getString("voice") as Voice;
|
||||||
await setUserVoice(interaction.user.id, voice);
|
await setUserVoice(interaction.user.id, voice);
|
||||||
|
|
||||||
await interaction.editReply("예주의 목소리를 변경했어요!");
|
await interaction.editReply("예주의 목소리를 변경했어요!");
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,29 @@
|
||||||
import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
import { defineCommand } from "../command";
|
import { defineCommand } from "../command";
|
||||||
import { skipCurrentVoice } from "../tts";
|
import { skipCurrentVoice } from "../tts";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName("스킵")
|
.setName("스킵")
|
||||||
.setDescription("실행중인 보이스를 건너뜁니다"),
|
.setDescription("실행중인 보이스를 건너뜁니다"),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!interaction.guild) {
|
if (!interaction.guild) {
|
||||||
await interaction.editReply("서버에서만 사용할 수 있어요");
|
await interaction.editReply("서버에서만 사용할 수 있어요");
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (await skipCurrentVoice(interaction.guild)) {
|
|
||||||
await interaction.editReply("스킵 되었어요");
|
|
||||||
} else {
|
|
||||||
await interaction.editReply("실행중인 보이스가 없어요");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
if (await skipCurrentVoice(interaction.guild)) {
|
||||||
|
await interaction.editReply("스킵 되었어요");
|
||||||
|
} else {
|
||||||
|
await interaction.editReply("실행중인 보이스가 없어요");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,42 @@
|
||||||
import { Channel, ChannelType, ChatInputCommandInteraction, MessageFlags, SlashCommandSubcommandBuilder } from "discord.js";
|
import {
|
||||||
import { defineCommand, DiscordCommand } from "../command";
|
type Channel,
|
||||||
import { insertGuildReadChannel, removeGuildReadChannel } from "../db";
|
ChannelType,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
MessageFlags,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from "discord.js";
|
||||||
|
import { defineCommand } from "../command";
|
||||||
|
import { removeGuildReadChannel } from "../db";
|
||||||
|
|
||||||
export default defineCommand(
|
export default defineCommand(
|
||||||
new SlashCommandSubcommandBuilder()
|
new SlashCommandSubcommandBuilder()
|
||||||
.setName("읽지마")
|
.setName("읽지마")
|
||||||
.setDescription("예주가 해당 채널을 더이상 읽지 않아요")
|
.setDescription("예주가 해당 채널을 더이상 읽지 않아요")
|
||||||
.addChannelOption(option =>
|
.addChannelOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("channel")
|
.setName("channel")
|
||||||
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildVoice)
|
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildVoice)
|
||||||
.setDescription("예주가 더이상 읽지 않을 채널이에요")
|
.setDescription("예주가 더이상 읽지 않을 채널이에요")
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
),
|
),
|
||||||
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
async (interaction: ChatInputCommandInteraction): Promise<any> => {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = interaction.options.getChannel("channel") as Channel;
|
const channel = interaction.options.getChannel("channel") as Channel;
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
|
||||||
if (guildId == null)
|
if (guildId == null)
|
||||||
return await interaction.editReply("알수없는 서버에요!");
|
return await interaction.editReply("알수없는 서버에요!");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeGuildReadChannel(guildId, channel.id);
|
await removeGuildReadChannel(guildId, channel.id);
|
||||||
} catch {
|
} catch {
|
||||||
return await interaction.editReply("읽지 않는것을 실패했어요 ?ㄴ");
|
return await interaction.editReply("읽지 않는것을 실패했어요 ?ㄴ");
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.editReply(`예주가 <#${channel.id}> 채널을 읽지않아요!`);
|
await interaction.editReply(`예주가 <#${channel.id}> 채널을 읽지않아요!`);
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,176 @@
|
||||||
import { prisma } from "../db/prisma";
|
import { prisma } from "../db/prisma";
|
||||||
import { DiscordUserProfile, DiscordGuildProfile, Voice } from "../db/generated/prisma/client";
|
import {
|
||||||
|
type DiscordUserProfile,
|
||||||
|
type DiscordGuildProfile,
|
||||||
|
Voice,
|
||||||
|
} from "../db/generated/prisma/client";
|
||||||
|
|
||||||
export function getUserProfile(userId: string): Promise<DiscordUserProfile> {
|
export function getUserProfile(userId: string): Promise<DiscordUserProfile> {
|
||||||
return prisma.discordUserProfile.upsert({
|
return prisma.discordUserProfile.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId: userId
|
userId: userId,
|
||||||
},
|
},
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserProfile(userId: string, profile: DiscordUserProfile): Promise<void> {
|
export async function setUserProfile(
|
||||||
await prisma.discordUserProfile.upsert({
|
userId: string,
|
||||||
where: {
|
profile: DiscordUserProfile,
|
||||||
userId: userId
|
): Promise<void> {
|
||||||
},
|
await prisma.discordUserProfile.upsert({
|
||||||
update: {
|
where: {
|
||||||
voice: profile.voice,
|
userId: userId,
|
||||||
nya: profile.nya
|
},
|
||||||
},
|
update: {
|
||||||
create: {
|
voice: profile.voice,
|
||||||
userId: userId,
|
nya: profile.nya,
|
||||||
}
|
},
|
||||||
});
|
create: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserNya(userId: string, nya: boolean): Promise<void> {
|
export async function setUserNya(userId: string, nya: boolean): Promise<void> {
|
||||||
await prisma.discordUserProfile.upsert({
|
await prisma.discordUserProfile.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId: userId
|
userId: userId,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
nya: nya
|
nya: nya,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserSupertonicStyle(userId: string, style: string): Promise<void> {
|
export async function setUserSupertonicStyle(
|
||||||
await prisma.discordUserProfile.upsert({
|
userId: string,
|
||||||
where: {
|
style: string,
|
||||||
userId: userId
|
): Promise<void> {
|
||||||
},
|
await prisma.discordUserProfile.upsert({
|
||||||
update: {
|
where: {
|
||||||
userSupertonicStyle: style
|
userId: userId,
|
||||||
},
|
},
|
||||||
create: {
|
update: {
|
||||||
userId: userId,
|
userSupertonicStyle: style,
|
||||||
userSupertonicStyle: style
|
},
|
||||||
}
|
create: {
|
||||||
});
|
userId: userId,
|
||||||
|
userSupertonicStyle: style,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserVoice(userId: string, voice: Voice): Promise<void> {
|
export async function setUserVoice(
|
||||||
await prisma.discordUserProfile.upsert({
|
userId: string,
|
||||||
where: {
|
voice: Voice,
|
||||||
userId: userId
|
): Promise<void> {
|
||||||
},
|
await prisma.discordUserProfile.upsert({
|
||||||
update: {
|
where: {
|
||||||
voice: voice
|
userId: userId,
|
||||||
},
|
},
|
||||||
create: {
|
update: {
|
||||||
userId: userId,
|
voice: voice,
|
||||||
voice: voice
|
},
|
||||||
}
|
create: {
|
||||||
});
|
userId: userId,
|
||||||
|
voice: voice,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserCanTypecast(userId: string, canTypecast: boolean): Promise<void> {
|
export async function setUserCanTypecast(
|
||||||
await prisma.discordUserProfile.upsert({
|
userId: string,
|
||||||
where: {
|
canTypecast: boolean,
|
||||||
userId: userId
|
): Promise<void> {
|
||||||
},
|
await prisma.discordUserProfile.upsert({
|
||||||
update: {
|
where: {
|
||||||
canTypecast: canTypecast
|
userId: userId,
|
||||||
},
|
},
|
||||||
create: {
|
update: {
|
||||||
userId: userId,
|
canTypecast: canTypecast,
|
||||||
}
|
},
|
||||||
});
|
create: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGuildProfile(guildId: string): Promise<DiscordGuildProfile> {
|
export function getGuildProfile(guildId: string): Promise<DiscordGuildProfile> {
|
||||||
return prisma.discordGuildProfile.upsert({
|
return prisma.discordGuildProfile.upsert({
|
||||||
where: { guildId: guildId },
|
where: { guildId: guildId },
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
guildId: guildId,
|
guildId: guildId,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasGuildReadChannel(
|
||||||
|
guildId: string,
|
||||||
|
channelId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
(await prisma.discordGuildProfile.findFirst({
|
||||||
|
where: {
|
||||||
|
guildId: guildId,
|
||||||
|
readChannel: { has: channelId },
|
||||||
|
},
|
||||||
|
})) != null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeGuildReadChannel(
|
||||||
|
guildId: string,
|
||||||
|
channelId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const guildProfile = await prisma.discordGuildProfile.findUnique({
|
||||||
|
where: {
|
||||||
|
guildId: guildId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (guildProfile) {
|
||||||
|
await prisma.discordGuildProfile.update({
|
||||||
|
where: {
|
||||||
|
guildId: guildId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
readChannel: guildProfile.readChannel.filter(
|
||||||
|
(channel) => channel != channelId,
|
||||||
|
),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasGuildReadChannel(guildId: string, channelId: string): Promise<boolean> {
|
export async function insertGuildReadChannel(
|
||||||
return (
|
guildId: string,
|
||||||
await prisma.discordGuildProfile.findFirst({
|
channelId: string,
|
||||||
where: {
|
): Promise<void> {
|
||||||
guildId: guildId,
|
await prisma.discordGuildProfile.upsert({
|
||||||
readChannel: { has: channelId }
|
where: {
|
||||||
}
|
guildId: guildId,
|
||||||
})
|
NOT: {
|
||||||
) != null;
|
readChannel: {
|
||||||
}
|
has: channelId,
|
||||||
|
},
|
||||||
export async function removeGuildReadChannel(guildId: string, channelId: string): Promise<void> {
|
},
|
||||||
const guildProfile = await prisma.discordGuildProfile.findUnique({
|
},
|
||||||
where: {
|
update: {
|
||||||
guildId: guildId
|
readChannel: {
|
||||||
},
|
push: channelId,
|
||||||
});
|
},
|
||||||
|
},
|
||||||
if (guildProfile) {
|
create: {
|
||||||
await prisma.discordGuildProfile.update({
|
guildId: guildId,
|
||||||
where: {
|
readChannel: [channelId],
|
||||||
guildId: guildId
|
},
|
||||||
},
|
});
|
||||||
data: {
|
|
||||||
readChannel: guildProfile.readChannel.filter(channel => channel != channelId),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function insertGuildReadChannel(guildId: string, channelId: string): Promise<void> {
|
|
||||||
await prisma.discordGuildProfile.upsert({
|
|
||||||
where: {
|
|
||||||
guildId: guildId,
|
|
||||||
NOT: {
|
|
||||||
readChannel: {
|
|
||||||
has: channelId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
readChannel: {
|
|
||||||
push: channelId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
guildId: guildId,
|
|
||||||
readChannel: [channelId],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import { ClientEvents } from "discord.js";
|
import { type ClientEvents } from "discord.js";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { requireDirectorySync } from "../utils/requireDirectory";
|
import { requireDirectory } from "../utils/requireDirectory";
|
||||||
|
|
||||||
export interface DiscordEvent<Event extends keyof ClientEvents> {
|
export interface DiscordEvent<Event extends keyof ClientEvents> {
|
||||||
event: Event
|
event: Event;
|
||||||
callback: (...args: ClientEvents[Event]) => Promise<void>
|
callback: (...args: ClientEvents[Event]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineEvent<Event extends keyof ClientEvents>(
|
export function defineEvent<Event extends keyof ClientEvents>(
|
||||||
event: Event,
|
event: Event,
|
||||||
callback: (...args: ClientEvents[Event]) => Promise<void>
|
callback: (...args: ClientEvents[Event]) => Promise<void>,
|
||||||
): DiscordEvent<Event> {
|
): DiscordEvent<Event> {
|
||||||
return {
|
return {
|
||||||
event: event,
|
event: event,
|
||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventDirectory = join(__dirname, "events");
|
export const eventDirectory = join(import.meta.dirname, "events");
|
||||||
export const eventMap = requireDirectorySync<DiscordEvent<any>>(eventDirectory);
|
export const eventMap =
|
||||||
|
await requireDirectory<DiscordEvent<any>>(eventDirectory);
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,69 @@
|
||||||
import { getOrCreateVoiceConnection } from "../util";
|
import { getOrCreateVoiceConnection } from "../util";
|
||||||
import { getUserProfile, hasGuildReadChannel } from "../db";
|
import { getUserProfile, hasGuildReadChannel } from "../db";
|
||||||
import { defineEvent } from "../event";
|
import { defineEvent } from "../event";
|
||||||
import { playVoice, PlayVoiceOptions } from "../tts";
|
import { playVoice, type PlayVoiceOptions } from "../tts";
|
||||||
import { Voice } from "../../db/generated/prisma/enums";
|
import { Voice } from "../../db/generated/prisma/enums";
|
||||||
import { SUPERTONIC_DEFAULT_VOICE } from "../../env";
|
import { SUPERTONIC_DEFAULT_VOICE } from "../../env";
|
||||||
|
|
||||||
export default defineEvent("messageCreate", async (message) => {
|
export default defineEvent("messageCreate", async (message) => {
|
||||||
if (message.author.bot)
|
if (message.author.bot) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const guild = message.guild;
|
const guild = message.guild;
|
||||||
if (guild == null)
|
if (guild == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const hasChannel = await hasGuildReadChannel(guild.id, message.channelId);
|
const hasChannel = await hasGuildReadChannel(guild.id, message.channelId);
|
||||||
if (!hasChannel)
|
if (!hasChannel) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const profile = await getUserProfile(message.author.id);
|
const profile = await getUserProfile(message.author.id);
|
||||||
|
|
||||||
let content = message.cleanContent;
|
let content = message.cleanContent;
|
||||||
let voice: Voice | null = null
|
let voice: Voice | null = null;
|
||||||
let options: PlayVoiceOptions = {};
|
const options: PlayVoiceOptions = {};
|
||||||
let matched: RegExpMatchArray | null = null;
|
// eslint-disable-next-line no-useless-assignment
|
||||||
if (content.startsWith("$t ")) {
|
let matched: RegExpMatchArray | null = null;
|
||||||
voice = "TypeCast";
|
if (content.startsWith("$t ")) {
|
||||||
} else if (content.startsWith("$p ")) {
|
voice = "TypeCast";
|
||||||
voice = "Papago";
|
} else if (content.startsWith("$p ")) {
|
||||||
} else if (matched = content.match(/^\$s(\S*) /)) {
|
voice = "Papago";
|
||||||
voice = "Supertonic";
|
} else if ((matched = content.match(/^\$s(\S*) /))) {
|
||||||
if (matched[1].length) {
|
voice = "Supertonic";
|
||||||
options.supertonicStyleId = matched[1]
|
if (matched[1]?.length) {
|
||||||
}
|
options.supertonicStyleId = matched[1];
|
||||||
} else if (content.match(/^\$\s/)) {
|
}
|
||||||
return;
|
} else if (content.match(/^\$\s/)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.userSupertonicStyle.length) {
|
||||||
|
options.supertonicStyleId ??= profile.userSupertonicStyle;
|
||||||
|
}
|
||||||
|
options.supertonicStyleId ??= SUPERTONIC_DEFAULT_VOICE;
|
||||||
|
|
||||||
|
if (voice) {
|
||||||
|
content = content.replace(/^\$\S+\s+/, "");
|
||||||
|
} else {
|
||||||
|
voice = profile.voice;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await getOrCreateVoiceConnection(guild))) return;
|
||||||
|
|
||||||
|
if (!guild.members.me?.voice.channel) return;
|
||||||
|
|
||||||
|
if (message.content === "") {
|
||||||
|
content =
|
||||||
|
message.attachments.size > 0
|
||||||
|
? `${message.attachments.size} 개의 첨부파일`
|
||||||
|
: "알수없는 메시지";
|
||||||
|
|
||||||
|
if (message.attachments.size == 1 && Math.random() < 0.05) {
|
||||||
|
content = "어이, 유저. 일개의 첨부파일이 뭘 할 수 있지?";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.userSupertonicStyle.length) {
|
await playVoice(guild, profile, voice, content, options);
|
||||||
options.supertonicStyleId ??= profile.userSupertonicStyle;
|
} catch (err) {
|
||||||
}
|
message.reply("말이 꼬이네요 ㅜ.ㅜ");
|
||||||
options.supertonicStyleId ??= SUPERTONIC_DEFAULT_VOICE;
|
console.log("playVoice failed. ", err);
|
||||||
|
}
|
||||||
if (voice) {
|
});
|
||||||
content = content.replace(/^\$\S+\s+/, "")
|
|
||||||
} else {
|
|
||||||
voice = profile.voice;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!await getOrCreateVoiceConnection(guild))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!guild.members.me?.voice.channel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (message.content === "") {
|
|
||||||
content = message.attachments.size > 0
|
|
||||||
? `${message.attachments.size} 개의 첨부파일`
|
|
||||||
: "알수없는 메시지"
|
|
||||||
|
|
||||||
if (message.attachments.size == 1 && Math.random() < 0.05) {
|
|
||||||
content = "어이, 유저. 일개의 첨부파일이 뭘 할 수 있지?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await playVoice(guild, profile, voice, content, options);
|
|
||||||
} catch(err) {
|
|
||||||
message.reply("말이 꼬이네요 ㅜ.ㅜ");
|
|
||||||
console.log("playVoice failed. ", err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { commandExecuteNameHashMap, DiscordCommand, DiscordCommandExecute } from "../command";
|
import { commandExecuteNameHashMap } from "../command";
|
||||||
import { defineEvent } from "../event";
|
import { defineEvent } from "../event";
|
||||||
|
|
||||||
export default defineEvent("interactionCreate", async (interaction) => {
|
export default defineEvent("interactionCreate", async (interaction) => {
|
||||||
if (!interaction.isChatInputCommand())
|
if (!interaction.isChatInputCommand()) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (commandExecuteNameHashMap[interaction.commandName]) {
|
if (commandExecuteNameHashMap[interaction.commandName]) {
|
||||||
await commandExecuteNameHashMap[interaction.commandName](interaction);
|
await commandExecuteNameHashMap[interaction.commandName]?.(interaction);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Client, Events, GatewayIntentBits, REST, Routes } from "discord.js";
|
import { Client, Events, GatewayIntentBits, REST, Routes } from "discord.js";
|
||||||
import { commandExecuteNameHashMap, defineCommands, DiscordCommand } from "./command";
|
import { commandExecuteNameHashMap, defineCommands } from "./command";
|
||||||
import { eventMap } from "./event";
|
import { eventMap } from "./event";
|
||||||
import { OutputHandler } from "../utils/outputHandler";
|
import { OutputHandler } from "../utils/outputHandler";
|
||||||
import { APPLICATION_ID } from "../env";
|
import { APPLICATION_ID } from "../env";
|
||||||
|
|
@ -8,93 +8,92 @@ import { join } from "node:path";
|
||||||
import { cwd } from "node:process";
|
import { cwd } from "node:process";
|
||||||
|
|
||||||
export class DiscordBot {
|
export class DiscordBot {
|
||||||
rest: REST;
|
rest: REST;
|
||||||
client: Client;
|
client: Client;
|
||||||
|
|
||||||
constructor(token: string) {
|
constructor(token: string) {
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
this.rest = new REST({ version: "10" }).setToken(token);
|
this.rest = new REST({ version: "10" }).setToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async registerCommands(): Promise<void> {
|
public async registerCommands(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!this.client.isReady()) {
|
if (!this.client.isReady()) {
|
||||||
await this.client.once(Events.ClientReady, () => {});
|
await this.client.once(Events.ClientReady, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandsCachePath = join(cwd(), "cache", "commands");
|
const commandsCachePath = join(cwd(), "cache", "commands");
|
||||||
await mkdir(commandsCachePath, {
|
await mkdir(commandsCachePath, {
|
||||||
recursive: true
|
recursive: true,
|
||||||
});
|
});
|
||||||
let commandsCache: { [key: string]: string }
|
let commandsCache: { [key: string]: string };
|
||||||
try {
|
try {
|
||||||
commandsCache = JSON.parse(
|
commandsCache = JSON.parse(
|
||||||
await readFile(join(commandsCachePath, "list.json"), "utf-8")
|
await readFile(join(commandsCachePath, "list.json"), "utf-8"),
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
commandsCache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [command, id] of Object.entries(commandsCache)) {
|
|
||||||
if (commandExecuteNameHashMap[command]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.log(`sync(delete) command: ${command}(${id})`);
|
|
||||||
await this.deleteCommand(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.rest.put(
|
|
||||||
Routes.applicationCommands(APPLICATION_ID),
|
|
||||||
{
|
|
||||||
body: defineCommands.map((command) => command.data.toJSON()),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
join(commandsCachePath, "list.json"),
|
|
||||||
JSON.stringify(
|
|
||||||
(
|
|
||||||
(await this.rest.get(Routes.applicationCommands(APPLICATION_ID))) as {
|
|
||||||
name: string,
|
|
||||||
id: string
|
|
||||||
}[]
|
|
||||||
).reduce<Record<string, string>>((acc, cur) => {
|
|
||||||
acc[cur.name] = cur.id;
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch(err) {
|
|
||||||
OutputHandler.errorLog("[Command Register Error]", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteCommand(commandId: string): Promise<void> {
|
|
||||||
if (!this.client.isReady())
|
|
||||||
throw new Error("Client is not ready");
|
|
||||||
|
|
||||||
await this.rest.delete(
|
|
||||||
Routes.applicationCommand(this.client.application.id, commandId)
|
|
||||||
);
|
);
|
||||||
}
|
} catch {
|
||||||
|
commandsCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
public registerEvents() {
|
for (const [command, id] of Object.entries(commandsCache)) {
|
||||||
try {
|
if (commandExecuteNameHashMap[command]) {
|
||||||
for (let index = 0; index < eventMap.length; index++) {
|
continue;
|
||||||
const event = eventMap[index];
|
|
||||||
this.client.on(event.event, event.callback);
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
OutputHandler.errorLog("[Event Register Error]", err);
|
|
||||||
}
|
}
|
||||||
|
console.log(`sync(delete) command: ${command}(${id})`);
|
||||||
|
await this.deleteCommand(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.rest.put(Routes.applicationCommands(APPLICATION_ID), {
|
||||||
|
body: defineCommands.map((command) => command.data.toJSON()),
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
join(commandsCachePath, "list.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
(
|
||||||
|
(await this.rest.get(
|
||||||
|
Routes.applicationCommands(APPLICATION_ID),
|
||||||
|
)) as {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[]
|
||||||
|
).reduce<Record<string, string>>((acc, cur) => {
|
||||||
|
acc[cur.name] = cur.id;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
OutputHandler.errorLog("[Command Register Error]", err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCommand(commandId: string): Promise<void> {
|
||||||
|
if (!this.client.isReady()) throw new Error("Client is not ready");
|
||||||
|
|
||||||
|
await this.rest.delete(
|
||||||
|
Routes.applicationCommand(this.client.application.id, commandId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerEvents() {
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < eventMap.length; index++) {
|
||||||
|
const event = eventMap[index];
|
||||||
|
if (!event) continue;
|
||||||
|
this.client.on(event.event, event.callback);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
OutputHandler.errorLog("[Event Register Error]", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,83 @@
|
||||||
import { AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, VoiceConnection } from "@discordjs/voice";
|
import {
|
||||||
import { stream as createStream } from "play-dl";
|
AudioPlayerStatus,
|
||||||
|
AudioResource,
|
||||||
|
createAudioPlayer,
|
||||||
|
createAudioResource,
|
||||||
|
VoiceConnection,
|
||||||
|
} from "@discordjs/voice";
|
||||||
import { Guild } from "discord.js";
|
import { Guild } from "discord.js";
|
||||||
import { getOrCreateVoiceConnection } from "./util";
|
import { getOrCreateVoiceConnection } from "./util";
|
||||||
import { OutputHandler } from "../utils/outputHandler";
|
import { OutputHandler } from "../utils/outputHandler";
|
||||||
import play from "play-dl";
|
import play from "play-dl";
|
||||||
|
|
||||||
namespace InitPlayDl {
|
namespace InitPlayDl {
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
export async function init() {
|
export async function init() {
|
||||||
if (initialized)
|
if (initialized) return;
|
||||||
return;
|
|
||||||
|
|
||||||
await play.getFreeClientID();
|
await play.getFreeClientID();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MusicQueue {
|
class MusicQueue {
|
||||||
private connection: VoiceConnection;
|
private connection: VoiceConnection;
|
||||||
private list: AudioResource[];
|
private list: AudioResource[];
|
||||||
constructor(connection: VoiceConnection) {
|
constructor(connection: VoiceConnection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.list = [];
|
this.list = [];
|
||||||
}
|
}
|
||||||
public static fromConnection(connection: VoiceConnection): MusicQueue {
|
public static fromConnection(connection: VoiceConnection): MusicQueue {
|
||||||
return (connection as any).queue ??= new MusicQueue(connection);
|
return ((connection as any).queue ??= new MusicQueue(connection));
|
||||||
}
|
}
|
||||||
private play() {
|
private play() {
|
||||||
if (!this.list[0]) return;
|
if (!this.list[0]) return;
|
||||||
const player = createAudioPlayer();
|
const player = createAudioPlayer();
|
||||||
this.connection.subscribe(player);
|
this.connection.subscribe(player);
|
||||||
player.once(AudioPlayerStatus.Idle, this.next.bind(this));
|
player.once(AudioPlayerStatus.Idle, this.next.bind(this));
|
||||||
player.play(this.list[0]);
|
player.play(this.list[0]);
|
||||||
}
|
}
|
||||||
public enqueue(resource: AudioResource) {
|
public enqueue(resource: AudioResource) {
|
||||||
this.list.push(resource);
|
this.list.push(resource);
|
||||||
if (this.list.length == 1) {
|
if (this.list.length == 1) {
|
||||||
this.play();
|
this.play();
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
public next() {
|
|
||||||
this.list.shift();
|
|
||||||
this.play();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
public next() {
|
||||||
|
this.list.shift();
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 토큰 설정하고 플레이돼게 만들기
|
// TODO: 토큰 설정하고 플레이돼게 만들기
|
||||||
export async function playMusic(guild: Guild, url: string) {
|
export async function playMusic(guild: Guild, url: string) {
|
||||||
try {
|
try {
|
||||||
const connection = await getOrCreateVoiceConnection(guild);
|
const connection = await getOrCreateVoiceConnection(guild);
|
||||||
if (!connection)
|
if (!connection) throw new Error("Yaeju is not joined VoiceChat");
|
||||||
throw new Error("Yaeju is not joined VoiceChat");
|
|
||||||
|
|
||||||
await InitPlayDl.init();
|
await InitPlayDl.init();
|
||||||
|
|
||||||
const validation = play.yt_validate(url);
|
const validation = play.yt_validate(url);
|
||||||
|
|
||||||
if (validation !== "video" && validation !== "playlist")
|
if (validation !== "video" && validation !== "playlist")
|
||||||
throw new Error("Invalid YouTube URL: " + validation);
|
throw new Error("Invalid YouTube URL: " + validation);
|
||||||
|
|
||||||
const stream = await play.stream(url);
|
const stream = await play.stream(url);
|
||||||
MusicQueue.fromConnection(connection).enqueue(
|
MusicQueue.fromConnection(connection).enqueue(
|
||||||
createAudioResource(stream.stream, {
|
createAudioResource(stream.stream, {
|
||||||
inputType: stream.type
|
inputType: stream.type,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
OutputHandler.errorLog("[PlayMusic Error]", err);
|
OutputHandler.errorLog("[PlayMusic Error]", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function skipMusic(guild: Guild) {
|
export async function skipMusic(guild: Guild) {
|
||||||
const connection = await getOrCreateVoiceConnection(guild);
|
const connection = await getOrCreateVoiceConnection(guild);
|
||||||
if (!connection)
|
if (!connection) throw new Error("Yaeju is not joined VoiceChat");
|
||||||
throw new Error("Yaeju is not joined VoiceChat");
|
|
||||||
|
|
||||||
MusicQueue.fromConnection(connection).next();
|
MusicQueue.fromConnection(connection).next();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,130 +1,145 @@
|
||||||
import { AudioPlayer, AudioPlayerStatus, AudioResource, createAudioPlayer, VoiceConnection } from "@discordjs/voice";
|
import {
|
||||||
|
AudioPlayer,
|
||||||
|
AudioPlayerStatus,
|
||||||
|
AudioResource,
|
||||||
|
createAudioPlayer,
|
||||||
|
VoiceConnection,
|
||||||
|
} from "@discordjs/voice";
|
||||||
import { Voice } from "../db/generated/prisma/enums";
|
import { Voice } from "../db/generated/prisma/enums";
|
||||||
import TTSTypecastModel from "../tts/typecast";
|
import TTSTypecastModel from "../tts/typecast";
|
||||||
import TTSPapagoModel from "../tts/papago";
|
import TTSPapagoModel from "../tts/papago";
|
||||||
import { Guild } from "discord.js";
|
import { Guild } from "discord.js";
|
||||||
import { getOrCreateVoiceConnection } from "./util";
|
import { getOrCreateVoiceConnection } from "./util";
|
||||||
import TTSModelBase from "../tts";
|
import TTSModelBase from "../tts";
|
||||||
import { DiscordUserProfile } from "../db/generated/prisma/client";
|
import { type DiscordUserProfile } from "../db/generated/prisma/client";
|
||||||
import { nyaize } from "../utils/nyaize";
|
import { nyaize } from "../utils/nyaize";
|
||||||
import { OutputHandler } from "../utils/outputHandler";
|
import { OutputHandler } from "../utils/outputHandler";
|
||||||
import TTSSupertonicModel from "../tts/supertonic";
|
import TTSSupertonicModel from "../tts/supertonic";
|
||||||
|
|
||||||
class VoiceQueue {
|
class VoiceQueue {
|
||||||
private connection: VoiceConnection;
|
private connection: VoiceConnection;
|
||||||
private list: AudioResource[];
|
private list: AudioResource[];
|
||||||
private currentPlayer?: AudioPlayer;
|
private currentPlayer?: AudioPlayer;
|
||||||
constructor(connection: VoiceConnection) {
|
constructor(connection: VoiceConnection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.list = [];
|
this.list = [];
|
||||||
}
|
}
|
||||||
public static fromConnection(connection: VoiceConnection): VoiceQueue {
|
public static fromConnection(connection: VoiceConnection): VoiceQueue {
|
||||||
return (connection as any).queue ??= new VoiceQueue(connection);
|
return ((connection as any).queue ??= new VoiceQueue(connection));
|
||||||
}
|
}
|
||||||
private play() {
|
private play() {
|
||||||
if (!this.list[0]) return;
|
if (!this.list[0]) return;
|
||||||
const player = this.currentPlayer = createAudioPlayer();
|
const player = (this.currentPlayer = createAudioPlayer());
|
||||||
this.connection.subscribe(player);
|
this.connection.subscribe(player);
|
||||||
player.once(AudioPlayerStatus.Idle, this.next.bind(this));
|
player.once(AudioPlayerStatus.Idle, this.next.bind(this));
|
||||||
player.play(this.list[0]);
|
player.play(this.list[0]);
|
||||||
}
|
}
|
||||||
public enqueue(resource: AudioResource) {
|
public enqueue(resource: AudioResource) {
|
||||||
this.list.push(resource);
|
this.list.push(resource);
|
||||||
if (this.list.length == 1) {
|
if (this.list.length == 1) {
|
||||||
this.play();
|
this.play();
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
public next() {
|
|
||||||
this.currentPlayer?.removeAllListeners(AudioPlayerStatus.Idle);
|
|
||||||
this.list.shift();
|
|
||||||
this.play();
|
|
||||||
}
|
|
||||||
public hasNext(): boolean {
|
|
||||||
return !!this.list[0];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
public next() {
|
||||||
|
this.currentPlayer?.removeAllListeners(AudioPlayerStatus.Idle);
|
||||||
|
this.list.shift();
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
public hasNext(): boolean {
|
||||||
|
return !!this.list[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlayVoiceOptions = {
|
export type PlayVoiceOptions = {
|
||||||
supertonicStyleId?: string,
|
supertonicStyleId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function playVoice(
|
export async function playVoice(
|
||||||
guild: Guild,
|
guild: Guild,
|
||||||
profile: DiscordUserProfile,
|
profile: DiscordUserProfile,
|
||||||
voice: Voice,
|
voice: Voice,
|
||||||
text: string,
|
text: string,
|
||||||
options?: PlayVoiceOptions,
|
options?: PlayVoiceOptions,
|
||||||
) {
|
) {
|
||||||
if (profile.nya)
|
if (profile.nya) text = nyaize(text);
|
||||||
text = nyaize(text);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let connection = await getOrCreateVoiceConnection(guild);
|
const connection = await getOrCreateVoiceConnection(guild);
|
||||||
if (!connection)
|
if (!connection) throw new Error("Yaeju is not joined VoiceChat");
|
||||||
throw new Error("Yaeju is not joined VoiceChat");
|
|
||||||
|
|
||||||
if (voice == "TypeCast" && !profile.canTypecast) {
|
if (voice == "TypeCast" && !profile.canTypecast) {
|
||||||
throw new Error(`the user ${profile.userId} is can't use typecast voice`);
|
throw new Error(`the user ${profile.userId} is can't use typecast voice`);
|
||||||
}
|
|
||||||
|
|
||||||
let voiceBufferList: Buffer[]
|
|
||||||
if (voice == "TypeCast") {
|
|
||||||
const content = TTSTypecastModel.instance.ttsify(text);
|
|
||||||
|
|
||||||
if (!content.length)
|
|
||||||
throw new Error("Empty content");
|
|
||||||
|
|
||||||
voiceBufferList = await Promise.all(content.split("\n").map(
|
|
||||||
(content) => TTSTypecastModel.instance.getMemcachedVoice(
|
|
||||||
TTSTypecastModel.instance.createRequestId(content)
|
|
||||||
)
|
|
||||||
));
|
|
||||||
} else if (voice == "Supertonic") {
|
|
||||||
const content = TTSSupertonicModel.instance.ttsify(text);
|
|
||||||
|
|
||||||
if (!content.length)
|
|
||||||
throw new Error("Empty content");
|
|
||||||
|
|
||||||
voiceBufferList = await Promise.all(content.split("\n").map(
|
|
||||||
(content) => TTSSupertonicModel.instance.getMemcachedVoice(
|
|
||||||
TTSSupertonicModel.instance.createRequestId(content, options?.supertonicStyleId)
|
|
||||||
)
|
|
||||||
));
|
|
||||||
} else if (voice == "Papago") {
|
|
||||||
const content = TTSPapagoModel.instance.ttsify(text);
|
|
||||||
if (!content.length)
|
|
||||||
throw new Error("Empty content");
|
|
||||||
|
|
||||||
voiceBufferList = await Promise.all(content.split("\n").map(
|
|
||||||
(content) => TTSPapagoModel.instance.getMemcachedVoice(
|
|
||||||
TTSPapagoModel.instance.createRequestId(content)
|
|
||||||
)
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown voice type: ${voice}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const voiceBuffer of voiceBufferList) {
|
|
||||||
VoiceQueue.fromConnection(connection).enqueue(
|
|
||||||
TTSModelBase.bufferToAudioResource(voiceBuffer)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
OutputHandler.errorLog("[PlayVoice Error]", err);
|
|
||||||
throw new Error(err as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let voiceBufferList: Buffer[];
|
||||||
|
if (voice == "TypeCast") {
|
||||||
|
const content = TTSTypecastModel.instance.ttsify(text);
|
||||||
|
|
||||||
|
if (!content.length) throw new Error("Empty content");
|
||||||
|
|
||||||
|
voiceBufferList = await Promise.all(
|
||||||
|
content
|
||||||
|
.split("\n")
|
||||||
|
.map((content) =>
|
||||||
|
TTSTypecastModel.instance.getMemcachedVoice(
|
||||||
|
TTSTypecastModel.instance.createRequestId(content),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (voice == "Supertonic") {
|
||||||
|
const content = TTSSupertonicModel.instance.ttsify(text);
|
||||||
|
|
||||||
|
if (!content.length) throw new Error("Empty content");
|
||||||
|
|
||||||
|
voiceBufferList = await Promise.all(
|
||||||
|
content
|
||||||
|
.split("\n")
|
||||||
|
.map((content) =>
|
||||||
|
TTSSupertonicModel.instance.getMemcachedVoice(
|
||||||
|
TTSSupertonicModel.instance.createRequestId(
|
||||||
|
content,
|
||||||
|
options?.supertonicStyleId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (voice == "Papago") {
|
||||||
|
const content = TTSPapagoModel.instance.ttsify(text);
|
||||||
|
if (!content.length) throw new Error("Empty content");
|
||||||
|
|
||||||
|
voiceBufferList = await Promise.all(
|
||||||
|
content
|
||||||
|
.split("\n")
|
||||||
|
.map((content) =>
|
||||||
|
TTSPapagoModel.instance.getMemcachedVoice(
|
||||||
|
TTSPapagoModel.instance.createRequestId(content),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown voice type: ${voice}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const voiceBuffer of voiceBufferList) {
|
||||||
|
VoiceQueue.fromConnection(connection).enqueue(
|
||||||
|
TTSModelBase.bufferToAudioResource(voiceBuffer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
OutputHandler.errorLog("[PlayVoice Error]", err);
|
||||||
|
throw new Error(err as any, { cause: err });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function skipCurrentVoice(guild: Guild): Promise<boolean> {
|
export async function skipCurrentVoice(guild: Guild): Promise<boolean> {
|
||||||
let connection = await getOrCreateVoiceConnection(guild);
|
const connection = await getOrCreateVoiceConnection(guild);
|
||||||
if (!connection)
|
if (!connection) throw new Error("YaejuNyang is not joined VoiceChat");
|
||||||
throw new Error("YaejuNyang is not joined VoiceChat");
|
|
||||||
|
|
||||||
const vqueue = VoiceQueue.fromConnection(connection);
|
const vqueue = VoiceQueue.fromConnection(connection);
|
||||||
if (vqueue.hasNext()) {
|
if (vqueue.hasNext()) {
|
||||||
vqueue.next();
|
vqueue.next();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,30 @@
|
||||||
import { getVoiceConnection as defaultGetVoiceConnection, EndBehaviorType, joinVoiceChannel, VoiceConnection } from "@discordjs/voice";
|
import {
|
||||||
|
getVoiceConnection as defaultGetVoiceConnection,
|
||||||
|
joinVoiceChannel,
|
||||||
|
VoiceConnection,
|
||||||
|
} from "@discordjs/voice";
|
||||||
import { Guild } from "discord.js";
|
import { Guild } from "discord.js";
|
||||||
|
|
||||||
export async function getOrCreateVoiceConnection(guild: Guild): Promise<VoiceConnection | undefined> {
|
export async function getOrCreateVoiceConnection(
|
||||||
let connection = defaultGetVoiceConnection(guild.id);
|
guild: Guild,
|
||||||
|
): Promise<VoiceConnection | undefined> {
|
||||||
|
let connection = defaultGetVoiceConnection(guild.id);
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
if (!guild.members.me?.voice.channel)
|
if (!guild.members.me?.voice.channel) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const channel = guild.members.me.voice.channel;
|
const channel = guild.members.me.voice.channel;
|
||||||
|
|
||||||
connection = joinVoiceChannel({
|
connection = joinVoiceChannel({
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
guildId: channel.guild.id,
|
guildId: channel.guild.id,
|
||||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||||
selfDeaf: false,
|
selfDeaf: false,
|
||||||
selfMute: false,
|
selfMute: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
}
|
}
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
import * as process from 'node:process'
|
import * as process from 'node:process'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/client"
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
import * as $Enums from "./enums"
|
import * as $Enums from "./enums"
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,10 @@ async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Modul
|
||||||
}
|
}
|
||||||
|
|
||||||
config.compilerWasm = {
|
config.compilerWasm = {
|
||||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.js"),
|
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.mjs"),
|
||||||
|
|
||||||
getQueryCompilerWasmModule: async () => {
|
getQueryCompilerWasmModule: async () => {
|
||||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.js")
|
const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.mjs")
|
||||||
return await decodeBase64AsWasm(wasm)
|
return await decodeBase64AsWasm(wasm)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import { config } from "dotenv"
|
import { config } from "dotenv";
|
||||||
|
|
||||||
config({ quiet: true });
|
config({ quiet: true });
|
||||||
|
|
||||||
export const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
export const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||||
export const APPLICATION_ID = process.env.APPLICATION_ID as string;
|
export const APPLICATION_ID = process.env.APPLICATION_ID as string;
|
||||||
export const GUILD_ID = process.env.GUILD_ID as string;
|
export const GUILD_ID = process.env.GUILD_ID as string;
|
||||||
export const TYPECAST_TOKENS = (process.env.TYPECAST_TOKEN as string).split(",");
|
export const TYPECAST_TOKENS = (process.env.TYPECAST_TOKEN as string).split(
|
||||||
|
",",
|
||||||
|
);
|
||||||
export const DATABASE_URL = process.env.DATABASE_URL as string;
|
export const DATABASE_URL = process.env.DATABASE_URL as string;
|
||||||
export const SUPERTONIC_DEFAULT_VOICE = (process.env.SUPERTONIC_DEFAULT_VOICE as string | undefined) ?? "Q1";
|
export const SUPERTONIC_DEFAULT_VOICE =
|
||||||
export const SUPERTONIC_STYLE_LIST: { name: string, value: string }[] = (()=>{
|
(process.env.SUPERTONIC_DEFAULT_VOICE as string | undefined) ?? "Q1";
|
||||||
const defaultValue = [
|
export const SUPERTONIC_STYLE_LIST: { name: string; value: string }[] = (() => {
|
||||||
{ name: "여성1", value: "F1" },
|
return (
|
||||||
];
|
(JSON.parse(process.env.SUPERTONIC_STYLE_LIST ?? "null") as any) ?? [
|
||||||
try {
|
{ name: "여성1", value: "F1" },
|
||||||
return JSON.parse(process.env.SUPERTONIC_STYLE_LIST ?? "null") as any ?? defaultValue
|
]
|
||||||
} catch {}
|
);
|
||||||
return defaultValue;
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,19 @@ import { APPLICATION_ID, DISCORD_TOKEN } from "./env";
|
||||||
export const bot = new DiscordBot(DISCORD_TOKEN);
|
export const bot = new DiscordBot(DISCORD_TOKEN);
|
||||||
|
|
||||||
bot.client.once("clientReady", async (client) => {
|
bot.client.once("clientReady", async (client) => {
|
||||||
await bot.registerCommands();
|
await bot.registerCommands();
|
||||||
await bot.registerEvents();
|
await bot.registerEvents();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"registerCommands: \n| " +
|
"registerCommands: \n| " +
|
||||||
(
|
(
|
||||||
(await client.rest.get(Routes.applicationCommands(APPLICATION_ID))) as any[]
|
(await client.rest.get(
|
||||||
).map(info => `name: ${info.name} id: ${info.id}`).join("\n| ")
|
Routes.applicationCommands(APPLICATION_ID),
|
||||||
);
|
)) as any[]
|
||||||
|
)
|
||||||
|
.map((info) => `name: ${info.name} id: ${info.id}`)
|
||||||
|
.join("\n| "),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.client.login(DISCORD_TOKEN);
|
bot.client.login(DISCORD_TOKEN);
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { writeFile, mkdir, stat, readFile } from "fs/promises";
|
import { writeFile, mkdir, readFile } from "fs/promises";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
|
import {
|
||||||
|
AudioResource,
|
||||||
|
createAudioResource,
|
||||||
|
StreamType,
|
||||||
|
} from "@discordjs/voice";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
@ -8,88 +12,84 @@ import { existsSync } from "node:fs";
|
||||||
import { saferKorean } from "../utils/saferKorean";
|
import { saferKorean } from "../utils/saferKorean";
|
||||||
|
|
||||||
export abstract class TTSModelBase<RequestId> {
|
export abstract class TTSModelBase<RequestId> {
|
||||||
public ttsify(input: string): string {
|
public ttsify(input: string): string {
|
||||||
return saferKorean(
|
return saferKorean(
|
||||||
input.replace(/:[^:]+:/g, (text: string): string => (TTSModelBase.EMOJI_MAP[text] ?? "이모지"))
|
input.replace(
|
||||||
);
|
/:[^:]+:/g,
|
||||||
|
(text: string): string => TTSModelBase.EMOJI_MAP[text] ?? "이모지",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public abstract createRequestId(text: string): RequestId;
|
||||||
|
public abstract getVoiceBuffer(id: RequestId): Promise<ArrayBuffer>;
|
||||||
|
public abstract getVoicePath(id: RequestId): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id로 부터 음성을 생성하여 캐시 파일에 저장합니다
|
||||||
|
* 생성된 음성을 반환합니다
|
||||||
|
*/
|
||||||
|
public async createVoice(id: RequestId, audioPath?: string): Promise<Buffer> {
|
||||||
|
const voiceBuffer = await this.getVoiceBuffer(id);
|
||||||
|
audioPath ??= this.getVoicePath(id);
|
||||||
|
const buffer = Buffer.from(voiceBuffer);
|
||||||
|
|
||||||
|
await mkdir(dirname(audioPath), { recursive: true });
|
||||||
|
await writeFile(audioPath, buffer);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* id로 부터 파일에 캐싱된 음성을 얻거나 없는 경우 생성합니다
|
||||||
|
*/
|
||||||
|
public async getVoice(id: RequestId, audioPath?: string): Promise<Buffer> {
|
||||||
|
audioPath ??= this.getVoicePath(id);
|
||||||
|
|
||||||
|
if (existsSync(audioPath)) {
|
||||||
|
const buffer = await readFile(audioPath);
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
public abstract createRequestId(text: string): RequestId
|
|
||||||
public abstract getVoiceBuffer(id: RequestId): Promise<ArrayBuffer>
|
|
||||||
public abstract getVoicePath(id: RequestId): string
|
|
||||||
|
|
||||||
/**
|
return this.createVoice(id, audioPath);
|
||||||
* id로 부터 음성을 생성하여 캐시 파일에 저장합니다
|
}
|
||||||
* 생성된 음성을 반환합니다
|
/**
|
||||||
*/
|
* id로 부터 메모리에 캐싱된 음성을 얻거나, 파일에 캐싱된
|
||||||
public async createVoice(id: RequestId, audioPath?: string): Promise<Buffer> {
|
* 음성을 얻거나, 없는 경우 생성합니다
|
||||||
const voiceBuffer = await this.getVoiceBuffer(id);
|
*/
|
||||||
audioPath ??= this.getVoicePath(id);
|
protected abstract cachedVoice: Map<string, Promise<Buffer>>;
|
||||||
const buffer = Buffer.from(voiceBuffer);
|
public async getMemcachedVoice(id: RequestId): Promise<Buffer> {
|
||||||
|
const path = this.getVoicePath(id);
|
||||||
|
|
||||||
await mkdir(dirname(audioPath), { recursive: true });
|
const cached = this.cachedVoice.get(path);
|
||||||
await writeFile(audioPath, buffer);
|
if (cached) {
|
||||||
|
return cached;
|
||||||
return buffer;
|
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* id로 부터 파일에 캐싱된 음성을 얻거나 없는 경우 생성합니다
|
|
||||||
*/
|
|
||||||
public async getVoice(id: RequestId, audioPath?: string): Promise<Buffer> {
|
|
||||||
audioPath ??= this.getVoicePath(id);
|
|
||||||
|
|
||||||
if (existsSync(audioPath)) {
|
const waitter = this.getVoice(id);
|
||||||
const buffer = await readFile(audioPath);
|
this.cachedVoice.set(path, waitter);
|
||||||
return buffer;
|
setTimeout(() => this.cachedVoice.delete(path), TTSModelBase.MemCacheTTL);
|
||||||
}
|
return await waitter;
|
||||||
|
}
|
||||||
return this.createVoice(id, audioPath);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* id로 부터 메모리에 캐싱된 음성을 얻거나, 파일에 캐싱된
|
|
||||||
* 음성을 얻거나, 없는 경우 생성합니다
|
|
||||||
*/
|
|
||||||
protected abstract cachedVoice: Map<String, Promise<Buffer>>
|
|
||||||
public async getMemcachedVoice(id: RequestId): Promise<Buffer> {
|
|
||||||
const path = this.getVoicePath(id);
|
|
||||||
|
|
||||||
const cached = this.cachedVoice.get(path);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitter = this.getVoice(id);
|
|
||||||
this.cachedVoice.set(path, waitter);
|
|
||||||
setTimeout(
|
|
||||||
() => this.cachedVoice.delete(path),
|
|
||||||
TTSModelBase.MemCacheTTL
|
|
||||||
);
|
|
||||||
return await waitter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export namespace TTSModelBase {
|
export namespace TTSModelBase {
|
||||||
export const EMOJI_MAP: { [key: string]: string } = {
|
export const EMOJI_MAP: { [key: string]: string } = {
|
||||||
":heart:": "하트",
|
":heart:": "하트",
|
||||||
":huck:": "헉헉!",
|
":huck:": "헉헉!",
|
||||||
":star:": "초롱초롱!"
|
":star:": "초롱초롱!",
|
||||||
}
|
};
|
||||||
export const AudioCachePath = join(
|
export const AudioCachePath = join(process.cwd(), "cache", "audio");
|
||||||
process.cwd(),
|
export function bufferToAudioResource(buf: Buffer): AudioResource {
|
||||||
"cache",
|
const stream = Readable.from(buf);
|
||||||
"audio",
|
const resource = createAudioResource(stream, {
|
||||||
);
|
inlineVolume: true,
|
||||||
export function bufferToAudioResource(buf: Buffer): AudioResource {
|
inputType: StreamType.Arbitrary,
|
||||||
const stream = Readable.from(buf);
|
});
|
||||||
const resource = createAudioResource(stream, {
|
|
||||||
inlineVolume: true,
|
|
||||||
inputType: StreamType.Arbitrary,
|
|
||||||
});
|
|
||||||
|
|
||||||
resource.volume?.setVolume(0.3);
|
resource.volume?.setVolume(0.3);
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
export function hashAudioFile(audio: string, suffix: string = ""): string {
|
export function hashAudioFile(audio: string, suffix: string = ""): string {
|
||||||
return createHash("md5").update(audio).digest("hex") + suffix + ".mp3";
|
return createHash("md5").update(audio).digest("hex") + suffix + ".mp3";
|
||||||
}
|
}
|
||||||
export const MemCacheTTL = 60 * 60 * 1000
|
export const MemCacheTTL = 60 * 60 * 1000;
|
||||||
}
|
}
|
||||||
export default TTSModelBase;
|
export default TTSModelBase;
|
||||||
|
|
|
||||||
|
|
@ -4,101 +4,116 @@ import fetch from "../utils/fetch";
|
||||||
import TTSModelBase from ".";
|
import TTSModelBase from ".";
|
||||||
|
|
||||||
export class TTSPapagoModel extends TTSModelBase<TTSPapagoModel.RequestId> {
|
export class TTSPapagoModel extends TTSModelBase<TTSPapagoModel.RequestId> {
|
||||||
protected cachedVoice: Map<String, Promise<Buffer>>
|
protected cachedVoice: Map<string, Promise<Buffer>>;
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super();
|
||||||
this.cachedVoice = new Map();
|
this.cachedVoice = new Map();
|
||||||
}
|
}
|
||||||
ttsify(input: string): string {
|
ttsify(input: string): string {
|
||||||
return super.ttsify(input)
|
return super.ttsify(input);
|
||||||
}
|
}
|
||||||
public getVoicePath(id: TTSPapagoModel.RequestId): string {
|
public getVoicePath(id: TTSPapagoModel.RequestId): string {
|
||||||
const audioFileName = TTSModelBase.hashAudioFile(id.text, `.${id.speaker}.${id.speed.replace(/\-/g, "_")}`);
|
const audioFileName = TTSModelBase.hashAudioFile(
|
||||||
const audioPath = join(
|
id.text,
|
||||||
TTSPapagoModel.PapagoAudioCachePath,
|
`.${id.speaker}.${id.speed.replace(/-/g, "_")}`,
|
||||||
audioFileName
|
);
|
||||||
);
|
const audioPath = join(TTSPapagoModel.PapagoAudioCachePath, audioFileName);
|
||||||
return audioPath;
|
return audioPath;
|
||||||
}
|
}
|
||||||
async getVoiceBuffer(id: TTSPapagoModel.RequestId, voiceId?: string): Promise<ArrayBuffer> {
|
async getVoiceBuffer(
|
||||||
voiceId ??= await TTSPapagoModel.getVoiceId(id)
|
id: TTSPapagoModel.RequestId,
|
||||||
const response = await fetch(`https://papago.naver.com/apis/tts/${voiceId}`);
|
voiceId?: string,
|
||||||
return await response.arrayBuffer();
|
): Promise<ArrayBuffer> {
|
||||||
}
|
voiceId ??= await TTSPapagoModel.getVoiceId(id);
|
||||||
createRequestId(text: string, speaker?: string, speed?: string): TTSPapagoModel.RequestId {
|
const response = await fetch(
|
||||||
return {
|
`https://papago.naver.com/apis/tts/${voiceId}`,
|
||||||
text,
|
);
|
||||||
speed: speed ?? "-1",
|
return await response.arrayBuffer();
|
||||||
speaker: speaker ?? "kyuri",
|
}
|
||||||
};
|
createRequestId(
|
||||||
}
|
text: string,
|
||||||
|
speaker?: string,
|
||||||
|
speed?: string,
|
||||||
|
): TTSPapagoModel.RequestId {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
speed: speed ?? "-1",
|
||||||
|
speaker: speaker ?? "kyuri",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export namespace TTSPapagoModel {
|
export namespace TTSPapagoModel {
|
||||||
export const instance = new TTSPapagoModel();
|
export const instance = new TTSPapagoModel();
|
||||||
export type RequestId = {
|
export type RequestId = {
|
||||||
speaker: string;
|
speaker: string;
|
||||||
speed: string;
|
speed: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
};
|
||||||
|
export const GenerateTokenKey = "v1.9.3_3bdf0438a8";
|
||||||
|
export function hmacMD5(key: string, plaintext: string) {
|
||||||
|
const hmac = createHmac("md5", key);
|
||||||
|
const data = hmac.update(plaintext);
|
||||||
|
|
||||||
|
return data.digest("base64");
|
||||||
|
}
|
||||||
|
export function generateToken(time: number) {
|
||||||
|
const e = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (e) => {
|
||||||
|
const t = ((time + 16 * Math.random()) % 16) | 0;
|
||||||
|
return (
|
||||||
|
(time = Math.floor(time / 16)),
|
||||||
|
("x" === e ? t : (3 & t) | 8).toString(16)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const plain = `${e}\n${"https://papago.naver.com/apis/tts/makeID"}\n${time}`;
|
||||||
|
|
||||||
|
return `PPG ${e}:${hmacMD5(GenerateTokenKey, plain)}`;
|
||||||
|
}
|
||||||
|
export async function getVoiceId(id: RequestId): Promise<string> {
|
||||||
|
const input = {
|
||||||
|
alpha: "0",
|
||||||
|
pitch: "0",
|
||||||
|
speaker: id.speaker,
|
||||||
|
speed: id.speed,
|
||||||
|
text: id.text,
|
||||||
};
|
};
|
||||||
export const GenerateTokenKey = "v1.9.3_3bdf0438a8";
|
|
||||||
export function hmacMD5(key: string, plaintext: string) {
|
|
||||||
const hmac = createHmac("md5", key);
|
|
||||||
const data = hmac.update(plaintext);
|
|
||||||
|
|
||||||
return data.digest("base64");
|
const time = new Date().getTime();
|
||||||
|
const token = TTSPapagoModel.generateToken(time);
|
||||||
|
|
||||||
|
const reqbody = new URLSearchParams(Object.entries(input)).toString();
|
||||||
|
const response = await fetch("https://papago.naver.com/apis/tts/makeID", {
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||||
|
Accept: "application/json",
|
||||||
|
"Accept-Language": "en",
|
||||||
|
"Sec-GPC": "1",
|
||||||
|
"Sec-Fetch-Dest": "empty",
|
||||||
|
"Sec-Fetch-Mode": "no-cors",
|
||||||
|
"Sec-Fetch-Site": "same-origin",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
Authorization: token,
|
||||||
|
Timestamp: time.toString(),
|
||||||
|
Pragma: "no-cache",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
referrer: "https://papago.naver.com/",
|
||||||
|
body: reqbody,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`TTS makeID request failed: ${response.status}: ${await response.text()}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export function generateToken(time: number) {
|
|
||||||
const e = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (e) => {
|
|
||||||
var t = (time + 16 * Math.random()) % 16 | 0;
|
|
||||||
return (time = Math.floor(time / 16)), ("x" === e ? t : (3 & t) | 8).toString(16);
|
|
||||||
});
|
|
||||||
|
|
||||||
const plain = `${e}\n${"https://papago.naver.com/apis/tts/makeID"}\n${time}`;
|
return ((await response.json()) as any).id;
|
||||||
|
}
|
||||||
return `PPG ${e}:${hmacMD5(GenerateTokenKey, plain)}`;
|
export const PapagoAudioCachePath = join(
|
||||||
}
|
TTSModelBase.AudioCachePath,
|
||||||
export async function getVoiceId(id: RequestId): Promise<string> {
|
"papago",
|
||||||
const input = {
|
);
|
||||||
alpha: "0",
|
|
||||||
pitch: "0",
|
|
||||||
speaker: id.speaker,
|
|
||||||
speed: id.speed,
|
|
||||||
text: id.text,
|
|
||||||
};
|
|
||||||
|
|
||||||
const time = new Date().getTime();
|
|
||||||
const token = TTSPapagoModel.generateToken(time);
|
|
||||||
|
|
||||||
const reqbody = new URLSearchParams(Object.entries(input)).toString();
|
|
||||||
const response = await fetch("https://papago.naver.com/apis/tts/makeID", {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|
||||||
Accept: "application/json",
|
|
||||||
"Accept-Language": "en",
|
|
||||||
"Sec-GPC": "1",
|
|
||||||
"Sec-Fetch-Dest": "empty",
|
|
||||||
"Sec-Fetch-Mode": "no-cors",
|
|
||||||
"Sec-Fetch-Site": "same-origin",
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
||||||
Authorization: token,
|
|
||||||
Timestamp: time.toString(),
|
|
||||||
Pragma: "no-cache",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
},
|
|
||||||
referrer: "https://papago.naver.com/",
|
|
||||||
body: reqbody,
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`TTS makeID request failed: ${response.status}: ${await response.text()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((await response.json()) as any).id;
|
|
||||||
}
|
|
||||||
export const PapagoAudioCachePath = join(
|
|
||||||
TTSModelBase.AudioCachePath,
|
|
||||||
"papago"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
export default TTSPapagoModel;
|
export default TTSPapagoModel;
|
||||||
|
|
|
||||||
|
|
@ -3,60 +3,67 @@ import fetch from "../utils/fetch";
|
||||||
import TTSModelBase from ".";
|
import TTSModelBase from ".";
|
||||||
|
|
||||||
export class TTSSupertonicModel extends TTSModelBase<TTSSupertonicModel.RequestId> {
|
export class TTSSupertonicModel extends TTSModelBase<TTSSupertonicModel.RequestId> {
|
||||||
protected override cachedVoice: Map<String, Promise<Buffer>>
|
protected override cachedVoice: Map<string, Promise<Buffer>>;
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super();
|
||||||
this.cachedVoice = new Map();
|
this.cachedVoice = new Map();
|
||||||
}
|
}
|
||||||
override ttsify(input: string): string {
|
override ttsify(input: string): string {
|
||||||
return super.ttsify(input);
|
return super.ttsify(input);
|
||||||
}
|
}
|
||||||
private async getSupertonicResponse(voiceId: TTSSupertonicModel.RequestId) {
|
private async getSupertonicResponse(voiceId: TTSSupertonicModel.RequestId) {
|
||||||
const payload = {
|
const payload = {
|
||||||
text: voiceId.text,
|
text: voiceId.text,
|
||||||
lang: "ko",
|
lang: "ko",
|
||||||
style_id: voiceId.styleId
|
style_id: voiceId.styleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!process.env.SUPERTONIC_API_URL) {
|
if (!process.env.SUPERTONIC_API_URL) {
|
||||||
throw Error("process.env.SUPERTONIC_API_URL not set");
|
throw Error("process.env.SUPERTONIC_API_URL not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fetch(process.env.SUPERTONIC_API_URL, {
|
return await fetch(process.env.SUPERTONIC_API_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async getVoiceBuffer(voiceId: TTSSupertonicModel.RequestId): Promise<ArrayBuffer> {
|
async getVoiceBuffer(
|
||||||
let response: Response | undefined;
|
voiceId: TTSSupertonicModel.RequestId,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const response: Response | undefined = (await this.getSupertonicResponse(
|
||||||
|
voiceId,
|
||||||
|
)) as Response;
|
||||||
|
if (response.ok) return await response.arrayBuffer();
|
||||||
|
|
||||||
response = await this.getSupertonicResponse(voiceId) as Response;
|
throw new Error(`invalid supertonic response ${await response.text()}`);
|
||||||
if (response.ok)
|
}
|
||||||
return await response.arrayBuffer();
|
public getVoicePath(id: TTSSupertonicModel.RequestId): string {
|
||||||
|
const audioFileName = TTSModelBase.hashAudioFile(id.text + id.styleId);
|
||||||
throw new Error(`invalid supertonic response ${await response.text()}`);
|
const audioPath = join(
|
||||||
}
|
TTSSupertonicModel.SupertonicAudioCachePath,
|
||||||
public getVoicePath(id: TTSSupertonicModel.RequestId): string {
|
audioFileName,
|
||||||
const audioFileName = TTSModelBase.hashAudioFile(id.text + id.styleId);
|
);
|
||||||
const audioPath = join(
|
return audioPath;
|
||||||
TTSSupertonicModel.SupertonicAudioCachePath,
|
}
|
||||||
audioFileName
|
public createRequestId(
|
||||||
);
|
text: string,
|
||||||
return audioPath;
|
styleId?: string,
|
||||||
}
|
): TTSSupertonicModel.RequestId {
|
||||||
public createRequestId(text: string, styleId?: string): TTSSupertonicModel.RequestId {
|
return {
|
||||||
return {
|
text,
|
||||||
text,
|
styleId: styleId ?? "F1",
|
||||||
styleId: styleId ?? "F1"
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export namespace TTSSupertonicModel {
|
export namespace TTSSupertonicModel {
|
||||||
export const instance = new TTSSupertonicModel();
|
export const instance = new TTSSupertonicModel();
|
||||||
export type RequestId = { text: string, styleId: string };
|
export type RequestId = { text: string; styleId: string };
|
||||||
export const SupertonicAudioCachePath = join(TTSModelBase.AudioCachePath, "supertonic");
|
export const SupertonicAudioCachePath = join(
|
||||||
|
TTSModelBase.AudioCachePath,
|
||||||
|
"supertonic",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export default TTSSupertonicModel;
|
export default TTSSupertonicModel;
|
||||||
|
|
|
||||||
|
|
@ -6,89 +6,108 @@ import { readFileSync, writeFileSync } from "fs";
|
||||||
import { cwd } from "process";
|
import { cwd } from "process";
|
||||||
|
|
||||||
export class TTSTypecastModel extends TTSModelBase<TTSTypecastModel.RequestId> {
|
export class TTSTypecastModel extends TTSModelBase<TTSTypecastModel.RequestId> {
|
||||||
protected cachedVoice: Map<String, Promise<Buffer>>
|
protected cachedVoice: Map<string, Promise<Buffer>>;
|
||||||
private lastUseApiKeyPath: string
|
private lastUseApiKeyPath: string;
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super();
|
||||||
this.cachedVoice = new Map();
|
this.cachedVoice = new Map();
|
||||||
this.lastUseApiKeyPath = join(cwd(), "cache", "typecast", "lastUseApiToken");
|
this.lastUseApiKeyPath = join(
|
||||||
}
|
cwd(),
|
||||||
ttsify(input: string): string {
|
"cache",
|
||||||
return super.ttsify(
|
"typecast",
|
||||||
input
|
"lastUseApiToken",
|
||||||
.replace(/ㅜㅜ/g, "눙물")
|
);
|
||||||
.replace(/빵/g, "빵 크크")
|
}
|
||||||
.replace(/[\?]+ *ㄴ/g, "물음표ㄴ")
|
ttsify(input: string): string {
|
||||||
)
|
return super.ttsify(
|
||||||
}
|
input
|
||||||
private async getTypecastResponse(apiKey: string, voiceId: TTSTypecastModel.RequestId) {
|
.replace(/ㅜㅜ/g, "눙물")
|
||||||
const payload = {
|
.replace(/빵/g, "빵 크크")
|
||||||
text: voiceId.text,
|
.replace(/[?]+ *ㄴ/g, "물음표ㄴ"),
|
||||||
model: "ssfm-v21",
|
);
|
||||||
voice_id: voiceId.voiceId,
|
}
|
||||||
language: "kor",
|
private async getTypecastResponse(
|
||||||
prompt: {
|
apiKey: string,
|
||||||
emotion_preset: "happy", // Options: normal, happy, sad, angry, tonemid, toneup
|
voiceId: TTSTypecastModel.RequestId,
|
||||||
emotion_intensity: 1 // Range: 0.0 to 2.0
|
) {
|
||||||
},
|
const payload = {
|
||||||
output: {
|
text: voiceId.text,
|
||||||
volume: 45, // Range: 0 to 200
|
model: "ssfm-v21",
|
||||||
audio_pitch: 1, // Range: -12 to +12 semitones
|
voice_id: voiceId.voiceId,
|
||||||
audio_tempo: 1, // Range: 0.5x to 2.0x
|
language: "kor",
|
||||||
audio_format: "mp3" // Options: wav, mp3
|
prompt: {
|
||||||
},
|
emotion_preset: "happy", // Options: normal, happy, sad, angry, tonemid, toneup
|
||||||
seed: 22 // For reproducible results
|
emotion_intensity: 1, // Range: 0.0 to 2.0
|
||||||
};
|
},
|
||||||
|
output: {
|
||||||
|
volume: 45, // Range: 0 to 200
|
||||||
|
audio_pitch: 1, // Range: -12 to +12 semitones
|
||||||
|
audio_tempo: 1, // Range: 0.5x to 2.0x
|
||||||
|
audio_format: "mp3", // Options: wav, mp3
|
||||||
|
},
|
||||||
|
seed: 22, // For reproducible results
|
||||||
|
};
|
||||||
|
|
||||||
return await fetch(TTSTypecastModel.TypecastApiUrl, {
|
return await fetch(TTSTypecastModel.TypecastApiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"X-API-KEY": apiKey,
|
"X-API-KEY": apiKey,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async getVoiceBuffer(voiceId: TTSTypecastModel.RequestId): Promise<ArrayBuffer> {
|
async getVoiceBuffer(
|
||||||
let response: Response | undefined;
|
voiceId: TTSTypecastModel.RequestId,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
let response: Response | undefined;
|
||||||
|
|
||||||
for (let i = 0; i < TYPECAST_TOKENS.length; i++) {
|
for (let i = 0; i < TYPECAST_TOKENS.length; i++) {
|
||||||
response = await this.getTypecastResponse(readFileSync(this.lastUseApiKeyPath, "utf-8"), voiceId) as Response;
|
response = (await this.getTypecastResponse(
|
||||||
|
readFileSync(this.lastUseApiKeyPath, "utf-8"),
|
||||||
|
voiceId,
|
||||||
|
)) as Response;
|
||||||
|
|
||||||
if (response.ok)
|
if (response.ok) return await response.arrayBuffer();
|
||||||
return await response.arrayBuffer();;
|
|
||||||
|
|
||||||
if (response.status === 402) {
|
if (response.status === 402) {
|
||||||
writeFileSync(this.lastUseApiKeyPath, TYPECAST_TOKENS[i]);
|
writeFileSync(this.lastUseApiKeyPath, TYPECAST_TOKENS[i]!);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`TTS makeID request failed: ${response.status}: ${await response.text()}`);
|
throw new Error(
|
||||||
}
|
`TTS makeID request failed: ${response.status}: ${await response.text()}`,
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Typecast Api use all credit");
|
|
||||||
}
|
|
||||||
public getVoicePath(id: TTSTypecastModel.RequestId): string {
|
|
||||||
const audioFileName = TTSModelBase.hashAudioFile(id.text);
|
|
||||||
const audioPath = join(
|
|
||||||
TTSTypecastModel.TypecastAudioCachePath,
|
|
||||||
id.voiceId,
|
|
||||||
audioFileName
|
|
||||||
);
|
);
|
||||||
return audioPath;
|
}
|
||||||
}
|
|
||||||
public createRequestId(text: string, voiceId?: string): TTSTypecastModel.RequestId {
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
voiceId: voiceId ?? TTSTypecastModel.DefaultVoiceId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("Typecast Api use all credit");
|
||||||
|
}
|
||||||
|
public getVoicePath(id: TTSTypecastModel.RequestId): string {
|
||||||
|
const audioFileName = TTSModelBase.hashAudioFile(id.text);
|
||||||
|
const audioPath = join(
|
||||||
|
TTSTypecastModel.TypecastAudioCachePath,
|
||||||
|
id.voiceId,
|
||||||
|
audioFileName,
|
||||||
|
);
|
||||||
|
return audioPath;
|
||||||
|
}
|
||||||
|
public createRequestId(
|
||||||
|
text: string,
|
||||||
|
voiceId?: string,
|
||||||
|
): TTSTypecastModel.RequestId {
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
voiceId: voiceId ?? TTSTypecastModel.DefaultVoiceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export namespace TTSTypecastModel {
|
export namespace TTSTypecastModel {
|
||||||
export const instance = new TTSTypecastModel();
|
export const instance = new TTSTypecastModel();
|
||||||
export type RequestId = { text: string, voiceId: string };
|
export type RequestId = { text: string; voiceId: string };
|
||||||
export const TypecastAudioCachePath = join(TTSModelBase.AudioCachePath, "typecast");
|
export const TypecastAudioCachePath = join(
|
||||||
export const TypecastApiUrl = "https://api.typecast.ai/v1/text-to-speech";
|
TTSModelBase.AudioCachePath,
|
||||||
export const DefaultVoiceId = "tc_6731b292d944a485bc406efb";
|
"typecast",
|
||||||
|
);
|
||||||
|
export const TypecastApiUrl = "https://api.typecast.ai/v1/text-to-speech";
|
||||||
|
export const DefaultVoiceId = "tc_6731b292d944a485bc406efb";
|
||||||
}
|
}
|
||||||
export default TTSTypecastModel;
|
export default TTSTypecastModel;
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
export default class CallingNumberKorean {
|
export default class CallingNumberKorean {
|
||||||
// 개, 살 이 붙는 경우 발음법
|
// 개, 살 이 붙는 경우 발음법
|
||||||
static SecondDigit = [
|
// prettier-ignore
|
||||||
"", "열", "스물", "서른", "마흔", "쉰",
|
static SecondDigit = [
|
||||||
"예순", "일흔", "여든", "아흔",
|
"", "열", "스물", "서른", "마흔", "쉰",
|
||||||
]
|
"예순", "일흔", "여든", "아흔",
|
||||||
static FirstDigit = [
|
];
|
||||||
"", "한", "두", "세", "네", "다섯",
|
// prettier-ignore
|
||||||
"여섯", "일곱", "여덟", "아홉", "열",
|
static FirstDigit = [
|
||||||
]
|
"", "한", "두", "세", "네", "다섯",
|
||||||
static canConvert(num: number): boolean {
|
"여섯", "일곱", "여덟", "아홉", "열",
|
||||||
return num < 100 && num >= 0 && Number.isInteger(num)
|
];
|
||||||
|
static canConvert(num: number): boolean {
|
||||||
|
return num < 100 && num >= 0 && Number.isInteger(num);
|
||||||
|
}
|
||||||
|
static convert(num: number): string {
|
||||||
|
const firstDigit = num % 10;
|
||||||
|
const secondDigit = Math.floor(num / 10);
|
||||||
|
|
||||||
|
let result = this.SecondDigit[secondDigit]! + this.FirstDigit[firstDigit]!;
|
||||||
|
|
||||||
|
if (!result.length) {
|
||||||
|
result = "영";
|
||||||
}
|
}
|
||||||
static convert(num: number): string {
|
|
||||||
const firstDigit = num % 10;
|
|
||||||
const secondDigit = Math.floor(num / 10);
|
|
||||||
|
|
||||||
let result = (
|
return result;
|
||||||
this.SecondDigit[secondDigit]
|
}
|
||||||
+ this.FirstDigit[firstDigit]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.length) {
|
|
||||||
result = "영"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1318
packages/utils/emoji-descriptions.json
Normal file
1318
packages/utils/emoji-descriptions.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,16 @@
|
||||||
export default async function(url: URL | RequestInfo, request: RequestInit={}, time: number=5000): Promise<Response> {
|
export default async function (
|
||||||
const controller = new AbortController();
|
url: URL | RequestInfo,
|
||||||
const timeout = setTimeout(() => controller.abort(), time);
|
request: RequestInit = {},
|
||||||
|
time: number = 5000,
|
||||||
|
): Promise<Response> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), time);
|
||||||
|
|
||||||
request.signal ??= controller.signal;
|
request.signal ??= controller.signal;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fetch(url, request);;
|
return await fetch(url, request);
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
export default class FloatKorean {
|
export default class FloatKorean {
|
||||||
static Digits = [
|
static Digits = ["영", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"];
|
||||||
"영", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"
|
static convert(num: string): string {
|
||||||
];
|
const buf = new Array(num.length);
|
||||||
static convert(num: string): string {
|
for (let idx = 0; idx < num.length; idx++) {
|
||||||
const buf = new Array(num.length);
|
if (num[idx] == ".") {
|
||||||
for (let idx = 0; idx < num.length; idx++) {
|
buf[idx] = "쩜";
|
||||||
if (num[idx] == ".") {
|
} else {
|
||||||
buf[idx] = "쩜";
|
buf[idx] = this.Digits[+(num[idx] ?? "0")];
|
||||||
} else {
|
}
|
||||||
buf[idx] = this.Digits[+num[idx]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.join("");
|
|
||||||
}
|
}
|
||||||
|
return buf.join("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,110 +1,141 @@
|
||||||
export default class IntegerKorean {
|
export default class IntegerKorean {
|
||||||
static DigitName = [ "영", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구" ];
|
static DigitName = [
|
||||||
static DigitModifier = ["", "십", "백", "천"];
|
"영",
|
||||||
static Unit = [ "", "만", "억", "조", "경", "해", "자", "양", "구", "간", "정", "재", "극", "항하사", "아승기", "나유타", "불가사의", "무량대수" ];
|
"일",
|
||||||
|
"이",
|
||||||
|
"삼",
|
||||||
|
"사",
|
||||||
|
"오",
|
||||||
|
"육",
|
||||||
|
"칠",
|
||||||
|
"팔",
|
||||||
|
"구",
|
||||||
|
];
|
||||||
|
static DigitModifier = ["", "십", "백", "천"];
|
||||||
|
static Unit = [
|
||||||
|
"",
|
||||||
|
"만",
|
||||||
|
"억",
|
||||||
|
"조",
|
||||||
|
"경",
|
||||||
|
"해",
|
||||||
|
"자",
|
||||||
|
"양",
|
||||||
|
"구",
|
||||||
|
"간",
|
||||||
|
"정",
|
||||||
|
"재",
|
||||||
|
"극",
|
||||||
|
"항하사",
|
||||||
|
"아승기",
|
||||||
|
"나유타",
|
||||||
|
"불가사의",
|
||||||
|
"무량대수",
|
||||||
|
];
|
||||||
|
|
||||||
private static stringifyKDigits(
|
private static stringifyKDigits(
|
||||||
first: number, second: number, third: number, forth: number
|
first: number,
|
||||||
): string {
|
second: number,
|
||||||
const buf = [];
|
third: number,
|
||||||
|
forth: number,
|
||||||
|
): string {
|
||||||
|
const buf = [];
|
||||||
|
|
||||||
if (forth) {
|
if (forth) {
|
||||||
if (forth >= 2) buf.push(this.DigitName[forth]);
|
if (forth >= 2) buf.push(this.DigitName[forth]);
|
||||||
buf.push(this.DigitModifier[3]);
|
buf.push(this.DigitModifier[3]);
|
||||||
}
|
|
||||||
if (third) {
|
|
||||||
if (third >= 2) buf.push(this.DigitName[third]);
|
|
||||||
buf.push(this.DigitModifier[2]);
|
|
||||||
}
|
|
||||||
if (second) {
|
|
||||||
if (second >= 2) buf.push(this.DigitName[second]);
|
|
||||||
buf.push(this.DigitModifier[1]);
|
|
||||||
}
|
|
||||||
if (first || (!forth && !third && !second)) {
|
|
||||||
buf.push(this.DigitName[first]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.join("");
|
|
||||||
}
|
}
|
||||||
private static parseKDigitsFromNumber(num: number): string {
|
if (third) {
|
||||||
const first = num % 10;
|
if (third >= 2) buf.push(this.DigitName[third]);
|
||||||
const second = Math.floor(num / 10) % 10;
|
buf.push(this.DigitModifier[2]);
|
||||||
const third = Math.floor(num / 100) % 10;
|
|
||||||
const forth = Math.floor(num / 1000) % 10;
|
|
||||||
|
|
||||||
return this.stringifyKDigits(first, second, third, forth);
|
|
||||||
}
|
}
|
||||||
private static parseKDigitsFromString(num: string, offset: number): string {
|
if (second) {
|
||||||
const first = +num[offset];
|
if (second >= 2) buf.push(this.DigitName[second]);
|
||||||
const second = offset >= 1 ? +num[offset - 1] : 0;
|
buf.push(this.DigitModifier[1]);
|
||||||
const third = offset >= 2 ? +num[offset - 2] : 0;
|
}
|
||||||
const forth = offset >= 3 ? +num[offset - 3] : 0;
|
if (first || (!forth && !third && !second)) {
|
||||||
|
buf.push(this.DigitName[first]);
|
||||||
return this.stringifyKDigits(first, second, third, forth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static convertFromString(num: string): string {
|
return buf.join("");
|
||||||
num = num.replace(/,/g, "");
|
}
|
||||||
let isNegative = false;
|
private static parseKDigitsFromNumber(num: number): string {
|
||||||
if (num.startsWith("-")) {
|
const first = num % 10;
|
||||||
num = num.slice(1, -1);
|
const second = Math.floor(num / 10) % 10;
|
||||||
isNegative = true;
|
const third = Math.floor(num / 100) % 10;
|
||||||
}
|
const forth = Math.floor(num / 1000) % 10;
|
||||||
if (num == "0") {
|
|
||||||
return isNegative ? "마이너스영" : "영";
|
|
||||||
}
|
|
||||||
|
|
||||||
const unitStack = [];
|
return this.stringifyKDigits(first, second, third, forth);
|
||||||
let offset = num.length - 1;
|
}
|
||||||
while (offset >= 0) {
|
private static parseKDigitsFromString(num: string, offset: number): string {
|
||||||
unitStack.push(this.parseKDigitsFromString(num, offset));
|
const first = +num[offset]!;
|
||||||
offset -= 4;
|
const second = offset >= 1 ? +num[offset - 1]! : 0;
|
||||||
}
|
const third = offset >= 2 ? +num[offset - 2]! : 0;
|
||||||
|
const forth = offset >= 3 ? +num[offset - 3]! : 0;
|
||||||
|
|
||||||
const buf = [];
|
return this.stringifyKDigits(first, second, third, forth);
|
||||||
if (isNegative) buf.push("마이너스");
|
}
|
||||||
for (let i = unitStack.length - 1; i >= 0; i--) {
|
|
||||||
const currUnit = this.Unit[i];
|
|
||||||
let currKDigits = unitStack[i];
|
|
||||||
|
|
||||||
if (currKDigits == "영") continue;
|
static convertFromString(num: string): string {
|
||||||
if (i == 1 && currKDigits == "일")
|
num = num.replace(/,/g, "");
|
||||||
currKDigits = "";
|
let isNegative = false;
|
||||||
|
if (num.startsWith("-")) {
|
||||||
buf.push(currKDigits + currUnit);
|
num = num.slice(1, -1);
|
||||||
}
|
isNegative = true;
|
||||||
|
|
||||||
return buf.join("");
|
|
||||||
}
|
}
|
||||||
static convertFromNumber(num: number): string {
|
if (num == "0") {
|
||||||
let isNegative = false;
|
return isNegative ? "마이너스영" : "영";
|
||||||
if (num < 0) {
|
|
||||||
isNegative = true;
|
|
||||||
num *= -1;
|
|
||||||
}
|
|
||||||
if (num == 0) {
|
|
||||||
return "영";
|
|
||||||
}
|
|
||||||
|
|
||||||
const unitStack = [];
|
|
||||||
while (num) {
|
|
||||||
unitStack.push(this.parseKDigitsFromNumber(num));
|
|
||||||
num = Math.floor(num / 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buf = [];
|
|
||||||
if (isNegative) buf.push("마이너스");
|
|
||||||
for (let i = unitStack.length - 1; i >= 0; i--) {
|
|
||||||
const currUnit = this.Unit[i];
|
|
||||||
let currKDigits = unitStack[i];
|
|
||||||
|
|
||||||
if (currKDigits == "영") continue;
|
|
||||||
if (i == 1 && currKDigits == "일")
|
|
||||||
currKDigits = "";
|
|
||||||
|
|
||||||
buf.push(currKDigits + currUnit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unitStack = [];
|
||||||
|
let offset = num.length - 1;
|
||||||
|
while (offset >= 0) {
|
||||||
|
unitStack.push(this.parseKDigitsFromString(num, offset));
|
||||||
|
offset -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = [];
|
||||||
|
if (isNegative) buf.push("마이너스");
|
||||||
|
for (let i = unitStack.length - 1; i >= 0; i--) {
|
||||||
|
const currUnit = this.Unit[i] ?? "";
|
||||||
|
let currKDigits = unitStack[i] ?? "";
|
||||||
|
|
||||||
|
if (currKDigits == "영") continue;
|
||||||
|
if (i == 1 && currKDigits == "일") currKDigits = "";
|
||||||
|
|
||||||
|
buf.push(currKDigits + currUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.join("");
|
||||||
|
}
|
||||||
|
static convertFromNumber(num: number): string {
|
||||||
|
let isNegative = false;
|
||||||
|
if (num < 0) {
|
||||||
|
isNegative = true;
|
||||||
|
num *= -1;
|
||||||
|
}
|
||||||
|
if (num == 0) {
|
||||||
|
return "영";
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitStack = [];
|
||||||
|
while (num) {
|
||||||
|
unitStack.push(this.parseKDigitsFromNumber(num));
|
||||||
|
num = Math.floor(num / 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = [];
|
||||||
|
if (isNegative) buf.push("마이너스");
|
||||||
|
for (let i = unitStack.length - 1; i >= 0; i--) {
|
||||||
|
const currUnit = this.Unit[i] ?? "";
|
||||||
|
let currKDigits = unitStack[i] ?? "";
|
||||||
|
|
||||||
|
if (currKDigits == "영") continue;
|
||||||
|
if (i == 1 && currKDigits == "일") currKDigits = "";
|
||||||
|
|
||||||
|
buf.push(currKDigits + currUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.join("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,137 +1,173 @@
|
||||||
export const nyaWords = {
|
export const nyaWords = {
|
||||||
'나': "냐",
|
나: "냐",
|
||||||
'낙': "냑",
|
낙: "냑",
|
||||||
'낚': "냒",
|
낚: "냒",
|
||||||
'낛': "냓",
|
낛: "냓",
|
||||||
'난': "냔",
|
난: "냔",
|
||||||
'낝': "냕",
|
낝: "냕",
|
||||||
'낞': "냖",
|
낞: "냖",
|
||||||
'낟': "냗",
|
낟: "냗",
|
||||||
'날': "냘",
|
날: "냘",
|
||||||
'낡': "냙",
|
낡: "냙",
|
||||||
'낢': "냚",
|
낢: "냚",
|
||||||
'낣': "냛",
|
낣: "냛",
|
||||||
'낤': "냜",
|
낤: "냜",
|
||||||
'낥': "냝",
|
낥: "냝",
|
||||||
'낦': "냞",
|
낦: "냞",
|
||||||
'낧': "냟",
|
낧: "냟",
|
||||||
'남': "냠",
|
남: "냠",
|
||||||
'납': "냡",
|
납: "냡",
|
||||||
'낪': "냢",
|
낪: "냢",
|
||||||
'낫': "냣",
|
낫: "냣",
|
||||||
'났': "냤",
|
났: "냤",
|
||||||
'낭': "냥",
|
낭: "냥",
|
||||||
'낮': "냦",
|
낮: "냦",
|
||||||
'낯': "냧",
|
낯: "냧",
|
||||||
'낰': "냨",
|
낰: "냨",
|
||||||
'낱': "냩",
|
낱: "냩",
|
||||||
'낲': "냪",
|
낲: "냪",
|
||||||
'낳': "냫",
|
낳: "냫",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const nyaWords2 = {
|
export const nyaWords2 = {
|
||||||
'내': "냥",
|
내: "냥",
|
||||||
'넹': "냥",
|
넹: "냥",
|
||||||
'넴': "냥",
|
넴: "냥",
|
||||||
'넵': "냥",
|
넵: "냥",
|
||||||
'냐': "냥",
|
냐: "냥",
|
||||||
'님': "냥",
|
님: "냥",
|
||||||
'니': "냥",
|
니: "냥",
|
||||||
'다': "다냥",
|
다: "다냥",
|
||||||
'까': "까냥",
|
까: "까냥",
|
||||||
'네': "네냥",
|
네: "네냥",
|
||||||
'야': "야냥",
|
야: "야냥",
|
||||||
'꺼': "꺼냥",
|
꺼: "꺼냥",
|
||||||
'래': "래냥",
|
래: "래냥",
|
||||||
'해': "해냥",
|
해: "해냥",
|
||||||
'지': "지냥",
|
지: "지냥",
|
||||||
'라': "라냥",
|
라: "라냥",
|
||||||
'요': "요냥",
|
요: "요냥",
|
||||||
'가': "가냥",
|
가: "가냥",
|
||||||
'데': "데냥",
|
데: "데냥",
|
||||||
'돼': "돼냥",
|
돼: "돼냥",
|
||||||
'줘': "줘냥",
|
줘: "줘냥",
|
||||||
'마': "마냥",
|
마: "마냥",
|
||||||
'와': "와냥",
|
와: "와냥",
|
||||||
'어': "어냥",
|
어: "어냥",
|
||||||
'자': "자냥",
|
자: "자냥",
|
||||||
'죠': "죠냥",
|
죠: "죠냥",
|
||||||
'서': "서냥",
|
서: "서냥",
|
||||||
'게': "게냥",
|
게: "게냥",
|
||||||
};
|
};
|
||||||
|
|
||||||
function replacePunctuation(input: string): string {
|
function replacePunctuation(input: string): string {
|
||||||
return input.replace(/(^|\s)([?!,.;~^@()]+)/g, (match, p1, p2) => {
|
return input.replace(/(^|\s)([?!,.;~^@()]+)/g, (match, p1, p2) => {
|
||||||
const firstChar = p2[0];
|
const firstChar = p2[0];
|
||||||
let transformed;
|
let transformed;
|
||||||
|
|
||||||
if (firstChar === '?') transformed = '냥?';
|
if (firstChar === "?") transformed = "냥?";
|
||||||
else if (firstChar === '!') transformed = '냥!';
|
else if (firstChar === "!") transformed = "냥!";
|
||||||
else if (firstChar === ',') transformed = '냥,';
|
else if (firstChar === ",") transformed = "냥,";
|
||||||
else if (firstChar === '.') transformed = '냥.';
|
else if (firstChar === ".") transformed = "냥.";
|
||||||
else if (firstChar === ';') transformed = '냥;';
|
else if (firstChar === ";") transformed = "냥;";
|
||||||
else if (firstChar === '~') transformed = '냥~';
|
else if (firstChar === "~") transformed = "냥~";
|
||||||
else if (firstChar === '^') transformed = '냥^';
|
else if (firstChar === "^") transformed = "냥^";
|
||||||
else if (firstChar === '@') transformed = '냥@';
|
else if (firstChar === "@") transformed = "냥@";
|
||||||
else if (firstChar === '(') transformed = '냥(';
|
else if (firstChar === "(") transformed = "냥(";
|
||||||
else if (firstChar === ')') transformed = '냥)';
|
else if (firstChar === ")") transformed = "냥)";
|
||||||
else transformed = p2;
|
else transformed = p2;
|
||||||
|
|
||||||
return p1 + transformed + p2.slice(1);
|
return p1 + transformed + p2.slice(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNyangAtMWord(sentence: string): string {
|
function addNyangAtMWord(sentence: string): string {
|
||||||
return sentence.split(' ').map((word) => {
|
return sentence
|
||||||
const match = word.match(/^([가-힣]+)([?!,.;~^@()]*)$/);
|
.split(" ")
|
||||||
|
.map((word) => {
|
||||||
|
const match = word.match(/^([가-힣]+)([?!,.;~^@()]*)$/);
|
||||||
|
|
||||||
if (!match) return word;
|
if (!match) return word;
|
||||||
|
|
||||||
const baseWord = match[1];
|
const baseWord = match[1] ?? "";
|
||||||
const punctuation = match[2];
|
const punctuation = match[2] ?? "";
|
||||||
|
|
||||||
const lastChar = baseWord[baseWord.length - 1];
|
const lastChar = baseWord[baseWord.length - 1] ?? "";
|
||||||
const charCode = lastChar.charCodeAt(0);
|
const charCode = lastChar.charCodeAt(0);
|
||||||
|
|
||||||
if (charCode >= 0xAC00 && charCode <= 0xD7A3) {
|
if (charCode >= 0xac00 && charCode <= 0xd7a3) {
|
||||||
const baseCode = charCode - 0xAC00;
|
const baseCode = charCode - 0xac00;
|
||||||
const jongseong = baseCode % 28;
|
const jongseong = baseCode % 28;
|
||||||
|
|
||||||
if (jongseong === 16) {
|
if (jongseong === 16) {
|
||||||
return baseWord + "냥" + punctuation;
|
return baseWord + "냥" + punctuation;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return word;
|
return word;
|
||||||
}).join(' ');
|
})
|
||||||
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nyaize(text: string): string {
|
export function nyaize(text: string): string {
|
||||||
for (let key in nyaWords2) {
|
for (const key in nyaWords2) {
|
||||||
text = text.replaceAll(key + ".", nyaWords2[key as keyof typeof nyaWords2] + "."); // yeah I gotta optimize these
|
text = text.replaceAll(
|
||||||
text = text.replaceAll(key + ",", nyaWords2[key as keyof typeof nyaWords2] + ",");
|
key + ".",
|
||||||
text = text.replaceAll(key + "?", nyaWords2[key as keyof typeof nyaWords2] + "?");
|
nyaWords2[key as keyof typeof nyaWords2] + ".",
|
||||||
text = text.replaceAll(key + "!", nyaWords2[key as keyof typeof nyaWords2] + "!");
|
); // yeah I gotta optimize these
|
||||||
text = text.replaceAll(key + ";", nyaWords2[key as keyof typeof nyaWords2] + ";");
|
text = text.replaceAll(
|
||||||
text = text.replaceAll(key + "~", nyaWords2[key as keyof typeof nyaWords2] + "~");
|
key + ",",
|
||||||
text = text.replaceAll(key + "^", nyaWords2[key as keyof typeof nyaWords2] + "^");
|
nyaWords2[key as keyof typeof nyaWords2] + ",",
|
||||||
text = text.replaceAll(key + "@", nyaWords2[key as keyof typeof nyaWords2] + "@");
|
);
|
||||||
text = text.replaceAll(key + "(", nyaWords2[key as keyof typeof nyaWords2] + "(");
|
text = text.replaceAll(
|
||||||
text = text.replaceAll(key + ")", nyaWords2[key as keyof typeof nyaWords2] + ")");
|
key + "?",
|
||||||
text = text.replaceAll(key + " ", nyaWords2[key as keyof typeof nyaWords2] + " ");
|
nyaWords2[key as keyof typeof nyaWords2] + "?",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + "!",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + "!",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + ";",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + ";",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + "~",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + "~",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + "^",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + "^",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + "@",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + "@",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + "(",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + "(",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + ")",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + ")",
|
||||||
|
);
|
||||||
|
text = text.replaceAll(
|
||||||
|
key + " ",
|
||||||
|
nyaWords2[key as keyof typeof nyaWords2] + " ",
|
||||||
|
);
|
||||||
|
|
||||||
if (text.endsWith(key)) {
|
if (text.endsWith(key)) {
|
||||||
text = text.slice(0, -1) + nyaWords2[key as keyof typeof nyaWords2];
|
text = text.slice(0, -1) + nyaWords2[key as keyof typeof nyaWords2];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let key in nyaWords) {
|
for (const key in nyaWords) {
|
||||||
text = text.replaceAll(key, nyaWords[key as keyof typeof nyaWords]);
|
text = text.replaceAll(key, nyaWords[key as keyof typeof nyaWords]);
|
||||||
}
|
}
|
||||||
|
|
||||||
text = replacePunctuation(text);
|
text = replacePunctuation(text);
|
||||||
|
|
||||||
text = addNyangAtMWord(text);
|
text = addNyangAtMWord(text);
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
@ -1,42 +1,37 @@
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { mkdir, open, readFile } from "fs/promises"
|
import { mkdir, open, readFile } from "fs/promises";
|
||||||
|
|
||||||
export namespace OutputHandler {
|
export namespace OutputHandler {
|
||||||
export const LogCachePath = join(
|
export const LogCachePath = join(process.cwd(), "cache", "log");
|
||||||
process.cwd(),
|
export const ErrorLogPath = join(LogCachePath, "error.log");
|
||||||
"cache",
|
export function getErrorOutput(...args: any[]) {
|
||||||
"log",
|
const timestamp = new Date().toISOString();
|
||||||
);
|
|
||||||
export const ErrorLogPath = join(LogCachePath, "error.log" );
|
|
||||||
export function getErrorOutput(...args: any[]) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
const message = args
|
const message = args
|
||||||
.map(arg => {
|
.map((arg) => {
|
||||||
if (arg instanceof Error) {
|
if (arg instanceof Error) {
|
||||||
return `${arg.name}: ${arg.message}\n${arg.stack}`;
|
return `${arg.name}: ${arg.message}\n${arg.stack}`;
|
||||||
}
|
}
|
||||||
if (typeof arg === 'object') {
|
if (typeof arg === "object") {
|
||||||
return JSON.stringify(arg);
|
return JSON.stringify(arg);
|
||||||
}
|
}
|
||||||
return String(arg);
|
return String(arg);
|
||||||
})
|
})
|
||||||
.join(' ');
|
.join(" ");
|
||||||
|
|
||||||
|
return `[${timestamp}] ${message}`;
|
||||||
|
}
|
||||||
|
export async function errorLog(...args: any[]): Promise<void> {
|
||||||
|
const output = getErrorOutput(...args);
|
||||||
|
console.log(output);
|
||||||
|
|
||||||
return `[${timestamp}] ${message}`;
|
await mkdir(dirname(ErrorLogPath), { recursive: true });
|
||||||
}
|
|
||||||
export async function errorLog(...args: any[]): Promise<void> {
|
|
||||||
const output = getErrorOutput(...args);
|
|
||||||
console.log(output);
|
|
||||||
|
|
||||||
await mkdir(dirname(ErrorLogPath), { recursive: true });
|
const fileHandle = await open(ErrorLogPath, "a");
|
||||||
|
fileHandle.write(output + "\n");
|
||||||
const fileHandle = await open(ErrorLogPath, "a");
|
fileHandle.close();
|
||||||
fileHandle.write(output + "\n");
|
}
|
||||||
fileHandle.close();
|
export async function getErrorLog(): Promise<string> {
|
||||||
}
|
return (await readFile(ErrorLogPath)).toString();
|
||||||
export async function getErrorLog(): Promise<string> {
|
}
|
||||||
return (await readFile(ErrorLogPath)).toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
export default class PhoneNumberKorean {
|
export default class PhoneNumberKorean {
|
||||||
static DigitName = [ "공", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구" ];
|
// prettier-ignore
|
||||||
static Dash = " ";
|
static DigitName = [
|
||||||
|
"공", "일", "이", "삼", "사",
|
||||||
|
"오", "육", "칠", "팔", "구",
|
||||||
|
];
|
||||||
|
static Dash = " ";
|
||||||
|
|
||||||
static convert(phone: string): string {
|
static convert(phone: string): string {
|
||||||
return phone.replace(/[\d\- \+]/g, (char: string) => {
|
return phone.replace(/[\d\- +]/g, (char: string) => {
|
||||||
if (char == "-") return PhoneNumberKorean.Dash;
|
if (char == "-") return PhoneNumberKorean.Dash;
|
||||||
if (char == " ") return " ";
|
if (char == " ") return " ";
|
||||||
if (char == "+") return "플러스";
|
if (char == "+") return "플러스";
|
||||||
return PhoneNumberKorean.DigitName[
|
return PhoneNumberKorean.DigitName[parseInt(char) as number] ?? "";
|
||||||
parseInt(char) as number
|
});
|
||||||
] ?? "";
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { readdirSync } from "fs";
|
|
||||||
import { readdir } from "fs/promises";
|
import { readdir } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
export async function requireDirectory<T>(directory: string): Promise<T[]> {
|
export async function requireDirectory<T>(directory: string): Promise<T[]> {
|
||||||
const requireFiles = (await readdir(directory)).filter(file => file.endsWith(".js"));
|
const requireFiles = (await readdir(directory)).filter((file) =>
|
||||||
return requireFiles.map(file => require(join(directory, file)).default as T);
|
file.endsWith(".js"),
|
||||||
}
|
);
|
||||||
|
return await Promise.all(
|
||||||
export function requireDirectorySync<T>(directory: string): T[] {
|
requireFiles.map(
|
||||||
const requireFiles = readdirSync(directory).filter(file => file.endsWith(".js"));
|
async (file) => (await import(join(directory, file))).default as T,
|
||||||
return requireFiles.map(file => require(join(directory, file)).default as T).filter(x=>x);
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,388 +1,393 @@
|
||||||
import CallingNumberKorean from "./callingNumberKorean";
|
import CallingNumberKorean from "./callingNumberKorean.js";
|
||||||
import FloatKorean from "./floatKorean";
|
import FloatKorean from "./floatKorean.js";
|
||||||
import IntegerKorean from "./integerKorean";
|
import IntegerKorean from "./integerKorean.js";
|
||||||
import PhoneNumberKorean from "./phoneNumberKorean";
|
import PhoneNumberKorean from "./phoneNumberKorean.js";
|
||||||
|
import EmojiDescriptions from "./emoji-descriptions.json" with { type: "json" };
|
||||||
|
|
||||||
export const IsolatedSymbolMap = {
|
export const IsolatedSymbolMap = {
|
||||||
"?": "물음표",
|
"?": "물음표",
|
||||||
"!": "느낌표",
|
"!": "느낌표",
|
||||||
"'": "쿼트",
|
"'": "쿼트",
|
||||||
"\"": "더블쿼트",
|
'"': "더블쿼트",
|
||||||
}
|
};
|
||||||
export const SymbolMap = {
|
export const SymbolMap = {
|
||||||
"%": "퍼센트",
|
"%": "퍼센트",
|
||||||
"$": "달러",
|
$: "달러",
|
||||||
"^": "캐럿",
|
"^": "캐럿",
|
||||||
"&": "엔드",
|
"&": "엔드",
|
||||||
"*": "스타",
|
"*": "스타",
|
||||||
"#": "샵",
|
"#": "샵",
|
||||||
"@": "엣",
|
"@": "엣",
|
||||||
".": "쩜",
|
".": "쩜",
|
||||||
"-": "마이너스",
|
"-": "마이너스",
|
||||||
"+": "플러스",
|
"+": "플러스",
|
||||||
"_": "언더바",
|
_: "언더바",
|
||||||
"=": "이퀄",
|
"=": "이퀄",
|
||||||
"/": "슬래쉬",
|
"/": "슬래쉬",
|
||||||
"~": "물결표",
|
"~": "물결표",
|
||||||
"\\": "역슬래쉬",
|
"\\": "역슬래쉬",
|
||||||
"♡": "하트 ",
|
"♡": "하트 ",
|
||||||
"|": "",
|
"|": "",
|
||||||
">": "",
|
">": "",
|
||||||
"<": "",
|
"<": "",
|
||||||
":": "콜론",
|
":": "콜론",
|
||||||
";": "세미콜론"
|
";": "세미콜론",
|
||||||
};
|
};
|
||||||
export const VersionPostfix = {
|
export const VersionPostfix = {
|
||||||
"a": "알파",
|
a: "알파",
|
||||||
"b": "베타",
|
b: "베타",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LangPrefixes = {
|
export const LangPrefixes = {
|
||||||
"typescript": "타입스크립트",
|
typescript: "타입스크립트",
|
||||||
"javascript": "자바스크립트",
|
javascript: "자바스크립트",
|
||||||
"java": "자바",
|
java: "자바",
|
||||||
"kotlin": "코틀린",
|
kotlin: "코틀린",
|
||||||
"rust": "러스트",
|
rust: "러스트",
|
||||||
"lua": "루아",
|
lua: "루아",
|
||||||
"json": "제이슨",
|
json: "제이슨",
|
||||||
"yaml": "야믈",
|
yaml: "야믈",
|
||||||
"yml": "야믈",
|
yml: "야믈",
|
||||||
"toml": "토믈",
|
toml: "토믈",
|
||||||
"xml": "엑스엠엘",
|
xml: "엑스엠엘",
|
||||||
"julia": "줄리아",
|
julia: "줄리아",
|
||||||
"matlab": "매트랩",
|
matlab: "매트랩",
|
||||||
"erlang": "얼랭",
|
erlang: "얼랭",
|
||||||
"elxir": "엘릭서",
|
elxir: "엘릭서",
|
||||||
"zig": "지그",
|
zig: "지그",
|
||||||
"txt": "텍스트",
|
txt: "텍스트",
|
||||||
"vim": "빔",
|
vim: "빔",
|
||||||
"perl": "펄",
|
perl: "펄",
|
||||||
"php": "피에이치피",
|
php: "피에이치피",
|
||||||
"lisp": "리스프",
|
lisp: "리스프",
|
||||||
"postscript": "포스트스크립트",
|
postscript: "포스트스크립트",
|
||||||
"ghostscript": "고스트스크립트",
|
ghostscript: "고스트스크립트",
|
||||||
"fortran": "포트란",
|
fortran: "포트란",
|
||||||
"algol": "알골",
|
algol: "알골",
|
||||||
"scala": "스칼라",
|
scala: "스칼라",
|
||||||
"haskell": "하스켈",
|
haskell: "하스켈",
|
||||||
"basic": "베이직",
|
basic: "베이직",
|
||||||
|
|
||||||
"cpp": "씨플플",
|
cpp: "씨플플",
|
||||||
"c++": "씨플플",
|
"c++": "씨플플",
|
||||||
"csharp": "씨샵",
|
csharp: "씨샵",
|
||||||
"cs": "씨샵",
|
cs: "씨샵",
|
||||||
"c#": "씨샵",
|
"c#": "씨샵",
|
||||||
"c": "씨",
|
c: "씨",
|
||||||
"h": "헤더",
|
h: "헤더",
|
||||||
|
|
||||||
"d": "디",
|
d: "디",
|
||||||
"awk": "에이더블류케이",
|
awk: "에이더블류케이",
|
||||||
"pl": "펄",
|
pl: "펄",
|
||||||
"pwsh": "파워쉘",
|
pwsh: "파워쉘",
|
||||||
"powershell": "파워쉘",
|
powershell: "파워쉘",
|
||||||
"cmd": "씨엠디",
|
cmd: "씨엠디",
|
||||||
"sh": "쉘",
|
sh: "쉘",
|
||||||
"ps1": "파워셀",
|
ps1: "파워셀",
|
||||||
"bat": "배치파일",
|
bat: "배치파일",
|
||||||
"bash": "베시스크립트",
|
bash: "베시스크립트",
|
||||||
"tex": "텍",
|
tex: "텍",
|
||||||
"dart": "다트",
|
dart: "다트",
|
||||||
"go": "고랭",
|
go: "고랭",
|
||||||
"python": "파이썬",
|
python: "파이썬",
|
||||||
"swift": "스위프트",
|
swift: "스위프트",
|
||||||
"css": "씨에스에스",
|
css: "씨에스에스",
|
||||||
"html": "에이치티엠엘",
|
html: "에이치티엠엘",
|
||||||
|
|
||||||
"latex": "레이텍",
|
latex: "레이텍",
|
||||||
"md": "마크다운",
|
md: "마크다운",
|
||||||
"markdown": "마크다운",
|
markdown: "마크다운",
|
||||||
|
|
||||||
"py": "파이썬",
|
py: "파이썬",
|
||||||
"hs": "하스켈",
|
hs: "하스켈",
|
||||||
"rs": "러스트",
|
rs: "러스트",
|
||||||
"kt": "코틀린",
|
kt: "코틀린",
|
||||||
"js": "자스",
|
js: "자스",
|
||||||
"ts": "타스",
|
ts: "타스",
|
||||||
"tsx": "리액트 타입스크립트",
|
tsx: "리액트 타입스크립트",
|
||||||
"jsx": "리액트 자바스크립트",
|
jsx: "리액트 자바스크립트",
|
||||||
"an": "에이엔",
|
an: "에이엔",
|
||||||
"parlance": "팔렌스",
|
parlance: "팔렌스",
|
||||||
};
|
};
|
||||||
export const LangPrefixMaxLength = (()=>{
|
export const LangPrefixMaxLength = (() => {
|
||||||
let max = 0;
|
let max = 0;
|
||||||
for (const key in LangPrefixes) {
|
for (const key in LangPrefixes) {
|
||||||
max = Math.max(key.length, max);
|
max = Math.max(key.length, max);
|
||||||
}
|
}
|
||||||
return max;
|
return max;
|
||||||
})();
|
})();
|
||||||
export const ChoseongMap = {
|
export const ChoseongMap = {
|
||||||
"ㄱ": "기역",
|
ㄱ: "기역",
|
||||||
"ㄴ": "니은",
|
ㄴ: "니은",
|
||||||
"ㄷ": "디귿",
|
ㄷ: "디귿",
|
||||||
"ㄹ": "리을",
|
ㄹ: "리을",
|
||||||
"ㅁ": "미음",
|
ㅁ: "미음",
|
||||||
"ㅂ": "비읍",
|
ㅂ: "비읍",
|
||||||
"ㅅ": "시옷",
|
ㅅ: "시옷",
|
||||||
"ㅇ": "이응",
|
ㅇ: "이응",
|
||||||
"ㅈ": "지읒",
|
ㅈ: "지읒",
|
||||||
"ㅊ": "치읓",
|
ㅊ: "치읓",
|
||||||
"ㅋ": "키읔",
|
ㅋ: "키읔",
|
||||||
"ㅌ": "티읕",
|
ㅌ: "티읕",
|
||||||
"ㅍ": "피읖",
|
ㅍ: "피읖",
|
||||||
"ㅎ": "히읗",
|
ㅎ: "히읗",
|
||||||
"ㄲ": "쌍기역",
|
ㄲ: "쌍기역",
|
||||||
"ㄸ": "쌍디귿",
|
ㄸ: "쌍디귿",
|
||||||
"ㅃ": "쌍비읍",
|
ㅃ: "쌍비읍",
|
||||||
"ㅆ": "쌍시옷",
|
ㅆ: "쌍시옷",
|
||||||
"ㅉ": "쌍지읒",
|
ㅉ: "쌍지읒",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SIPrefix = {
|
export const SIPrefix = {
|
||||||
"k": "킬로",
|
k: "킬로",
|
||||||
"ki": "키비",
|
ki: "키비",
|
||||||
"m": "메가",
|
m: "메가",
|
||||||
"mi": "메비",
|
mi: "메비",
|
||||||
"g": "기가",
|
g: "기가",
|
||||||
"gi": "기비",
|
gi: "기비",
|
||||||
"t": "테라",
|
t: "테라",
|
||||||
"ti": "테비",
|
ti: "테비",
|
||||||
"p": "페타",
|
p: "페타",
|
||||||
"pi": "페비",
|
pi: "페비",
|
||||||
"e": "엑사",
|
e: "엑사",
|
||||||
"ei": "엑시",
|
ei: "엑시",
|
||||||
"z": "제타",
|
z: "제타",
|
||||||
"zi": "제비",
|
zi: "제비",
|
||||||
"y": "요타",
|
y: "요타",
|
||||||
"yi": "요비",
|
yi: "요비",
|
||||||
};
|
};
|
||||||
export const LiterPrefix = {
|
export const LiterPrefix = {
|
||||||
"m": "밀리",
|
m: "밀리",
|
||||||
"": "",
|
"": "",
|
||||||
};
|
};
|
||||||
export const MeterPrefix = {
|
export const MeterPrefix = {
|
||||||
"m": "밀리",
|
m: "밀리",
|
||||||
"c": "센치",
|
c: "센치",
|
||||||
"": "",
|
"": "",
|
||||||
"k": "킬로",
|
k: "킬로",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GIFMap = {
|
export const GIFMap = {
|
||||||
"tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-windy-hair-gif-19187698": "화난 일레이나",
|
"tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-windy-hair-gif-19187698":
|
||||||
"tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-sparkle-amazed-gif-18827847": "일레이나 반짝반짝!",
|
"화난 일레이나",
|
||||||
"images-ext-1.discordapp.net/external/C3xPFuUxs16jY25AR3NvsIDezaOtib9wozhLBWejZk4/https/media.tenor.com/bUd8mk4ufwsAAAPo/anime-disappointment.mp4": "일레이나 절래절래",
|
"tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-sparkle-amazed-gif-18827847":
|
||||||
"images-ext-1.discordapp.net/external/SXv4qgpy2r1Gx-dNxhcfJle6AXDaH_SToRjEBYYaup0/https/media.tenor.com/nDDxJc4FDwEAAAPo/cute.mp4": "일레이나 끄덕",
|
"일레이나 반짝반짝!",
|
||||||
"tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-what-gif-19011602": "당황한 일레이나",
|
"images-ext-1.discordapp.net/external/C3xPFuUxs16jY25AR3NvsIDezaOtib9wozhLBWejZk4/https/media.tenor.com/bUd8mk4ufwsAAAPo/anime-disappointment.mp4":
|
||||||
"images-ext-1.discordapp.net/external/2R41WcvNJwYMD69UKls2cDa_hEL-rzCRCFvOi2DDOVo/https/media.tenor.com/sU3RCOixDbgAAAPo/majo-no-tabitabi-the-journey-of-elaina.mp4": "일레이나 손짓",
|
"일레이나 절래절래",
|
||||||
|
"images-ext-1.discordapp.net/external/SXv4qgpy2r1Gx-dNxhcfJle6AXDaH_SToRjEBYYaup0/https/media.tenor.com/nDDxJc4FDwEAAAPo/cute.mp4":
|
||||||
|
"일레이나 끄덕",
|
||||||
|
"tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-what-gif-19011602":
|
||||||
|
"당황한 일레이나",
|
||||||
|
"images-ext-1.discordapp.net/external/2R41WcvNJwYMD69UKls2cDa_hEL-rzCRCFvOi2DDOVo/https/media.tenor.com/sU3RCOixDbgAAAPo/majo-no-tabitabi-the-journey-of-elaina.mp4":
|
||||||
|
"일레이나 손짓",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UnicodeEmojis = {
|
export const UnicodeSymbols = {
|
||||||
"㎢": "제곱킬로미터",
|
"㎢": "제곱킬로미터",
|
||||||
"㎡": "제곱미터",
|
"㎡": "제곱미터",
|
||||||
"↑": "위쪽 화살표", "↓": "아래쪽 화살표",
|
"↑": "위쪽 화살표",
|
||||||
"←": "왼쪽 화살표", "→": "오른쪽 화살표",
|
"↓": "아래쪽 화살표",
|
||||||
"↔": "좌우 화살표",
|
"←": "왼쪽 화살표",
|
||||||
"↖": "왼쪽 위 화살표", "↗": "오른쪽 위 화살표",
|
"→": "오른쪽 화살표",
|
||||||
"↘": "오른쪽 아래 화살표", "↙": "왼쪽 아래 화살표",
|
"↔": "좌우 화살표",
|
||||||
"🎀": "리본", "🐱": "고양이", "✨": "반짝임", "🍞": "빵",
|
"↖": "왼쪽 위 화살표",
|
||||||
"🧸": "인형", "🍓": "딸기", "🌸": "벚꽃", "🍰": "조각 케이크",
|
"↗": "오른쪽 위 화살표",
|
||||||
"🐾": "발자국", "👑": "왕관", "🦄": "유니콘", "🐰": "토끼",
|
"↘": "오른쪽 아래 화살표",
|
||||||
"🦊": "여우", "🐻": "곰", "🐼": "판다", "🐥": "아기 병아리",
|
"↙": "왼쪽 아래 화살표",
|
||||||
"🦋": "나비", "🌹": "장미", "🌷": "튤립", "🍀": "네잎클로버",
|
};
|
||||||
"🍁": "단풍잎", "🌙": "초승달", "⭐": "별", "🌈": "무지개",
|
export const UnicodeSymbolsRegex = new RegExp(
|
||||||
"🌋": "화산", "🌊": "파도", "🔮": "수정구슬", "🍬": "사탕",
|
"[" + Object.keys(UnicodeSymbols).join() + "]",
|
||||||
"🍭": "막대사탕", "🍫": "초콜릿", "🍩": "도넛", "🍪": "쿠키",
|
"gu",
|
||||||
"🍨": "아이스크림", "🥞": "팬케이크", "🍎": "빨간 사과",
|
|
||||||
"🍒": "체리", "🍑": "복숭아", "🍇": "포도", "🧁": "컵케이크",
|
|
||||||
"🍋": "레몬", "🍌": "바나나", "🥑": "아보카도", "🥕": "당근",
|
|
||||||
"🍕": "피자", "🍔": "햄버거", "🍟": "감자튀김", "🍿": "팝콘",
|
|
||||||
"🧂": "소금", "🎈": "풍선", "🎉": "폭죽", "🎬": "슬레이트",
|
|
||||||
"🎁": "선물", "🎫": "티켓", "🏆": "트로피", "🎨": "팔레트",
|
|
||||||
"🎤": "마이크", "📱": "휴대전화", "🎼": "높은음자리표",
|
|
||||||
"🎸": "기타", "🎧": "헤드폰", "🎹": "키보드", "💻": "노트북",
|
|
||||||
"⌚": "시계", "📷": "카메라", "🔍": "돋보기", "💡": "전구",
|
|
||||||
"🕯️": "양초", "📜": "두루마리", "🔑": "열쇠", "🔒": "자물쇠",
|
|
||||||
"🔔": "종", "📣": "메가폰", "📦": "상자", "✉️": "편지",
|
|
||||||
"📌": "압정", "✂️": "가위", "🩹": "반창고", "🧬": "DNA",
|
|
||||||
"🧪": "시험관", "🔭": "망원경", "🚀": "로켓", "🛸": "UFO",
|
|
||||||
"🚲": "자전거", "🛹": "스케이트보드", "⚓": "닻", "⛺": "텐트",
|
|
||||||
"🧭": "나침반", "🗺️": "세계지도", "🏡": "집", "🏰": "성",
|
|
||||||
"🎡": "관람차", "🎠": "회전목마", "⛲": "분수", "💎": "보석",
|
|
||||||
"🪞": "거울", "💄": "립스틱",
|
|
||||||
}
|
|
||||||
export const UnicodeEmojisRegex = new RegExp(
|
|
||||||
"(" +
|
|
||||||
Object.keys(UnicodeEmojis).join(")|(")
|
|
||||||
+ ")", "g"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export function processDots(input: string): string {
|
export function processDots(input: string): string {
|
||||||
return input.replace(/[\.,]+$/, "")
|
return input
|
||||||
.replace(/[\.,]{2,}/g, "")
|
.replace(/[.,]+$/, "")
|
||||||
.replace(/[\.,]\s/g, " ");
|
.replace(/[.,]{2,}/g, "")
|
||||||
|
.replace(/[.,]\s/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saferKorean(input: string): string {
|
export function saferKorean(input: string): string {
|
||||||
return processDots(input.normalize() + " ")
|
return (
|
||||||
// Process isolated symbols
|
processDots(input.normalize() + " ")
|
||||||
.replace(/^[\?\!\'\"]+ $/, (total)=>(
|
// Process isolated symbols
|
||||||
[...total].map(element => IsolatedSymbolMap[
|
.replace(/^[?!'"]+ $/, (total) =>
|
||||||
element as keyof typeof IsolatedSymbolMap
|
[...total]
|
||||||
]).join("")
|
.map(
|
||||||
))
|
(element) =>
|
||||||
.replace(/\s\|\|\s/g, " 오얼 ")
|
IsolatedSymbolMap[element as keyof typeof IsolatedSymbolMap],
|
||||||
.replace(/\s\&\&\s/g, " 엔드 ")
|
)
|
||||||
|
.join(""),
|
||||||
|
)
|
||||||
|
.replace(/\s\|\|\s/g, " 오얼 ")
|
||||||
|
.replace(/\s&&\s/g, " 엔드 ")
|
||||||
|
|
||||||
// Process codeblock
|
// Process codeblock
|
||||||
.replace(/\`\`\`([\s\S]*?)\`\`\`/g, (_, content: string)=>{
|
.replace(/```([\s\S]*?)```/g, (_, content: string) => {
|
||||||
const code = content.substring(0, LangPrefixMaxLength).toLowerCase();
|
const code = content.substring(0, LangPrefixMaxLength).toLowerCase();
|
||||||
let lang = "";
|
let lang = "";
|
||||||
for (const [key, value] of Object.entries(LangPrefixes)) {
|
for (const [key, value] of Object.entries(LangPrefixes)) {
|
||||||
if (code.startsWith(key + "\n")) {
|
if (code.startsWith(key + "\n")) {
|
||||||
lang = value + " ";
|
lang = value + " ";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lang + "코드블럭";
|
return lang + "코드블럭";
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process link
|
// Process link
|
||||||
.replace(/[hH][tT]{2}[pP][sS]?:\/\/(\S+)/g, (_, url: string) => {
|
.replace(/[hH][tT]{2}[pP][sS]?:\/\/(\S+)/g, (_, url: string) => {
|
||||||
const mapped = GIFMap[url as keyof typeof GIFMap] as (string | undefined);
|
const mapped = GIFMap[url as keyof typeof GIFMap] as string | undefined;
|
||||||
if (mapped) return mapped;
|
if (mapped) return mapped;
|
||||||
|
|
||||||
if (url.startsWith("tenor.com/view")) {
|
if (url.startsWith("tenor.com/view")) {
|
||||||
return "움짤!";
|
return "움짤!";
|
||||||
}
|
}
|
||||||
return "링크";
|
return "링크";
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process koreans
|
// Process koreans
|
||||||
.replace(/[아ㅏ]{3,}/g, "아아아")
|
.replace(/[아ㅏ]{3,}/g, "아아아")
|
||||||
.replace(/ㄹㅇ/g, (content: string) => {
|
.replace(/ㄹㅇ/g, (content: string) => {
|
||||||
return "리얼".repeat(
|
return "리얼".repeat(Math.min(Math.floor(content.length / 2), 2));
|
||||||
Math.min(Math.floor(content.length / 2), 2)
|
})
|
||||||
|
.replace(/(ㅇㄴ)+/g, (content: string) => {
|
||||||
|
return "아니".repeat(Math.min(Math.floor(content.length / 2), 2));
|
||||||
|
})
|
||||||
|
.replace(/(ㅇㅎ)+/g, (content: string) => {
|
||||||
|
return "아하".repeat(Math.min(Math.floor(content.length / 2), 2));
|
||||||
|
})
|
||||||
|
.replace(/(ㅇㅋ)+/g, (content: string) => {
|
||||||
|
return "오키".repeat(Math.min(Math.floor(content.length / 2), 2));
|
||||||
|
})
|
||||||
|
.replace(/(ㅊㅋ)+/g, (content: string) => {
|
||||||
|
return "추카".repeat(Math.min(Math.floor(content.length / 2), 2));
|
||||||
|
})
|
||||||
|
.replace(/ㄱ+/g, (content: string) => {
|
||||||
|
if (content.length == 2) {
|
||||||
|
return "고고";
|
||||||
|
} else if (content.length == 3) {
|
||||||
|
return "고고고";
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
})
|
||||||
|
.replace(/ㅋ{2,}/g, (content) => "크".repeat(content.length))
|
||||||
|
.replace(/ㅌ{2,}/g, "틔틔")
|
||||||
|
.replace(/ㄷ{2,}/g, "덜덜")
|
||||||
|
.replace(/ㄴ{2,}/g, "노노")
|
||||||
|
.replace(/ㅇ{2,}/g, "응응")
|
||||||
|
.replace(/ㅊ{2,}/g, "추추")
|
||||||
|
.replace(/ㅠ{2,}/g, "유유")
|
||||||
|
.replace(/ㅜ{2,}/g, "우우")
|
||||||
|
.replace(
|
||||||
|
/[ㄱ-ㅎㄲㄸㅃㅆㅉ]/g,
|
||||||
|
(char: string) => ChoseongMap[char as keyof typeof ChoseongMap],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Process number, unit
|
||||||
|
.replace(
|
||||||
|
/(\+\d+[\s-]+)?([\d-]+)/g,
|
||||||
|
(_, prefix: string | undefined, phone: string) => {
|
||||||
|
const all = (prefix ?? "") + phone;
|
||||||
|
if (!phone.includes("-")) return all;
|
||||||
|
return PhoneNumberKorean.convert(all);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/([\d,]+)([kKMmgGtTpPeEzZyY][iI]?)[bB]/g,
|
||||||
|
(_, num: string, mod: string) => {
|
||||||
|
// 10kib => 십키비바이트
|
||||||
|
num = IntegerKorean.convertFromString(num);
|
||||||
|
mod = SIPrefix[mod.toLowerCase() as keyof typeof SIPrefix];
|
||||||
|
return `${num} ${mod}바이트 `;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(/([\d,]+)([m]?)[lL]\s/g, (_, num: string, mod: string) => {
|
||||||
|
// 10l => 십리터
|
||||||
|
num = IntegerKorean.convertFromString(num);
|
||||||
|
mod = LiterPrefix[mod as keyof typeof LiterPrefix];
|
||||||
|
return `${num} ${mod}리터 `;
|
||||||
|
})
|
||||||
|
.replace(/([\d,]+)([mck]?)m\s/g, (_, num: string, mod: string) => {
|
||||||
|
// 10m => 십미터
|
||||||
|
num = IntegerKorean.convertFromString(num);
|
||||||
|
mod = MeterPrefix[mod as keyof typeof MeterPrefix];
|
||||||
|
return `${num} ${mod}미터 `;
|
||||||
|
})
|
||||||
|
.replace(
|
||||||
|
/([\d.]+)\s*([개살시평명])/g,
|
||||||
|
(_, num: string, postfix: string) => {
|
||||||
|
// 10명 => 열명
|
||||||
|
if (num.includes(".")) {
|
||||||
|
return num + postfix;
|
||||||
|
}
|
||||||
|
const intNum = parseInt(num);
|
||||||
|
if (CallingNumberKorean.canConvert(intNum)) {
|
||||||
|
return CallingNumberKorean.convert(intNum) + postfix;
|
||||||
|
} else {
|
||||||
|
return IntegerKorean.convertFromString(num) + postfix;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(/[\d,]+/g, (num: string) => {
|
||||||
|
// 1,000 원 => 천원
|
||||||
|
if (!num.includes(",")) return num;
|
||||||
|
return IntegerKorean.convertFromString(num);
|
||||||
|
})
|
||||||
|
.replace(
|
||||||
|
/(v?)([\d.]+)([ab]?)/g,
|
||||||
|
(_, suffix: string, num: string, postfix: string) => {
|
||||||
|
const dotCount = [...num.matchAll(/\./g)].length;
|
||||||
|
const hasNoSuffix = suffix == "";
|
||||||
|
|
||||||
|
if (hasNoSuffix && dotCount == 0) {
|
||||||
|
// 일반 숫자는 인트로 읽음
|
||||||
|
return IntegerKorean.convertFromString(num) + postfix;
|
||||||
|
} else if (hasNoSuffix && dotCount == 1) {
|
||||||
|
// 소수는 . 앞은 인트로, 뒤는 플로트로 읽음
|
||||||
|
const [intPart, floatPart] = num.split(/\./);
|
||||||
|
return (
|
||||||
|
IntegerKorean.convertFromString(intPart ?? "") +
|
||||||
|
"쩜" +
|
||||||
|
FloatKorean.convert(floatPart ?? "") +
|
||||||
|
postfix
|
||||||
);
|
);
|
||||||
})
|
} else if ((suffix == "v" || postfix.length) && dotCount > 1) {
|
||||||
.replace(/(ㅇㄴ)+/g, (content: string) => {
|
// 버전표기는 버전을 붙여서
|
||||||
return "아니".repeat(
|
return (
|
||||||
Math.min(Math.floor(content.length / 2), 2)
|
"버전" +
|
||||||
|
FloatKorean.convert(num) +
|
||||||
|
(VersionPostfix[postfix as keyof typeof VersionPostfix] ?? "")
|
||||||
);
|
);
|
||||||
})
|
} else {
|
||||||
.replace(/(ㅇㅎ)+/g, (content: string) => {
|
// 모든 경우에 속하지 않으면 영일이삼사 형태로 읽음
|
||||||
return "아하".repeat(
|
// (예: 111.111.111.111 ip address)
|
||||||
Math.min(Math.floor(content.length / 2), 2)
|
return FloatKorean.convert(num) + postfix;
|
||||||
);
|
}
|
||||||
})
|
},
|
||||||
.replace(/(ㅇㅋ)+/g, (content: string) => {
|
)
|
||||||
return "오키".repeat(
|
|
||||||
Math.min(Math.floor(content.length / 2), 2)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.replace(/(ㅊㅋ)+/g, (content: string) => {
|
|
||||||
return "추카".repeat(
|
|
||||||
Math.min(Math.floor(content.length / 2), 2)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.replace(/ㄱ+/g, (content: string) => {
|
|
||||||
if (content.length == 2) {
|
|
||||||
return "고고";
|
|
||||||
} else if (content.length == 3) {
|
|
||||||
return "고고고";
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
})
|
|
||||||
.replace(/ㅋ{2,}/g, (content) => "크".repeat(content.length))
|
|
||||||
.replace(/ㅌ{2,}/g, "틔틔")
|
|
||||||
.replace(/ㄷ{2,}/g, "덜덜")
|
|
||||||
.replace(/ㄴ{2,}/g, "노노")
|
|
||||||
.replace(/ㅇ{2,}/g, "응응")
|
|
||||||
.replace(/ㅊ{2,}/g, "추추")
|
|
||||||
.replace(/ㅠ{2,}/g, "유유")
|
|
||||||
.replace(/ㅜ{2,}/g, "우우")
|
|
||||||
.replace(/[ㄱ-ㅎㄲㄸㅃㅆㅉ]/g, (char: string) => ChoseongMap[char as keyof typeof ChoseongMap])
|
|
||||||
|
|
||||||
// Process number, unit
|
// Process symbol
|
||||||
.replace(/(\+\d+[\s\-]+)?([\d\-]+)/g, (_, prefix: string | undefined, phone: string) => {
|
.replace(
|
||||||
const all = (prefix ?? "") + phone;
|
/[%^&*#@.\-+_=/\\♡$|:;><]/g,
|
||||||
if (!phone.includes("-")) return all;
|
(t) => SymbolMap[t as keyof typeof SymbolMap],
|
||||||
return PhoneNumberKorean.convert(all);
|
)
|
||||||
})
|
.replace(/([?!]+)/g, (_, content: string): string => content[0] ?? "")
|
||||||
.replace(/([\d,]+)([kKMmgGtTpPeEzZyY][iI]?)[bB]/g, (_, num: string, mod: string) => {
|
.replace(/[ \t\f\r]+/g, " ")
|
||||||
// 10kib => 십키비바이트
|
|
||||||
num = IntegerKorean.convertFromString(num);
|
|
||||||
mod = SIPrefix[mod.toLowerCase() as keyof typeof SIPrefix];
|
|
||||||
return `${num} ${mod}바이트 `;
|
|
||||||
})
|
|
||||||
.replace(/([\d,]+)([m]?)[lL]\s/g, (_, num: string, mod: string) => {
|
|
||||||
// 10l => 십리터
|
|
||||||
num = IntegerKorean.convertFromString(num);
|
|
||||||
mod = LiterPrefix[mod as keyof typeof LiterPrefix];
|
|
||||||
return `${num} ${mod}리터 `;
|
|
||||||
})
|
|
||||||
.replace(/([\d,]+)([mck]?)m\s/g, (_, num: string, mod: string) => {
|
|
||||||
// 10m => 십미터
|
|
||||||
num = IntegerKorean.convertFromString(num);
|
|
||||||
mod = MeterPrefix[mod as keyof typeof MeterPrefix];
|
|
||||||
return `${num} ${mod}미터 `;
|
|
||||||
})
|
|
||||||
.replace(/([\d\.]+)\s*([개살시평명])/g, (_, num: string, postfix: string)=>{
|
|
||||||
// 10명 => 열명
|
|
||||||
if (num.includes(".")) {
|
|
||||||
return num + postfix;
|
|
||||||
}
|
|
||||||
const intNum = parseInt(num)
|
|
||||||
if (CallingNumberKorean.canConvert(intNum)) {
|
|
||||||
return CallingNumberKorean.convert(intNum) + postfix;
|
|
||||||
} else {
|
|
||||||
return IntegerKorean.convertFromString(num) + postfix;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.replace(/[\d,]+/g, (num: string) => {
|
|
||||||
// 1,000 원 => 천원
|
|
||||||
if (!num.includes(",")) return num;
|
|
||||||
return IntegerKorean.convertFromString(num);
|
|
||||||
})
|
|
||||||
.replace(/(v?)([\d\.]+)([ab]?)/g, (_, suffix: string, num: string, postfix: string) => {
|
|
||||||
const dotCount = [...num.matchAll(/\./g)].length;
|
|
||||||
const hasNoSuffix = suffix == "";
|
|
||||||
|
|
||||||
if (hasNoSuffix && dotCount == 0) {
|
// Process emoji
|
||||||
// 일반 숫자는 인트로 읽음
|
.replace(
|
||||||
return IntegerKorean.convertFromString(num) + postfix;
|
UnicodeSymbolsRegex,
|
||||||
} else if (hasNoSuffix && dotCount == 1) {
|
(content: string) =>
|
||||||
// 소수는 . 앞은 인트로, 뒤는 플로트로 읽음
|
UnicodeSymbols[content as keyof typeof UnicodeSymbols] ?? content,
|
||||||
const [intPart, floatPart] = num.split(/\./);
|
)
|
||||||
return (
|
.replace(/\p{Extended_Pictographic}/gu, (content: string) => {
|
||||||
IntegerKorean.convertFromString(intPart)
|
return (
|
||||||
+ "쩜"
|
EmojiDescriptions[content as keyof typeof EmojiDescriptions] ??
|
||||||
+ FloatKorean.convert(floatPart)
|
content
|
||||||
+ postfix
|
);
|
||||||
)
|
})
|
||||||
} else if ((suffix == "v" || postfix.length) && dotCount > 1) {
|
.replace(/\p{Emoji}/u, " 이모지 ")
|
||||||
// 버전표기는 버전을 붙여서
|
.trim()
|
||||||
return (
|
);
|
||||||
"버전"
|
|
||||||
+ FloatKorean.convert(num)
|
|
||||||
+ (VersionPostfix[
|
|
||||||
postfix as keyof typeof VersionPostfix
|
|
||||||
] ?? "")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 모든 경우에 속하지 않으면 영일이삼사 형태로 읽음
|
|
||||||
// (예: 111.111.111.111 ip address)
|
|
||||||
return FloatKorean.convert(num) + postfix;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process symbol
|
|
||||||
.replace(/[\%\^\&\*\#\@\.\-\+\_\=\/\\♡\$\|\:\;\>\<]/g, (t) => (
|
|
||||||
SymbolMap[t as keyof typeof SymbolMap]
|
|
||||||
))
|
|
||||||
.replace(/([\?\!]+)/g, (_, content: string) => content[0])
|
|
||||||
.replace(/[ \t\f\r]+/g, " ")
|
|
||||||
|
|
||||||
// Process emoji
|
|
||||||
.replace(UnicodeEmojisRegex, (content: string) => (UnicodeEmojis[content as keyof typeof UnicodeEmojis] ?? content))
|
|
||||||
.replace(/\p{Emoji}/u, " 이모지 ")
|
|
||||||
.trim()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import "dotenv/config";
|
||||||
import { defineConfig } from "prisma/config";
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "prisma/schema.prisma",
|
schema: "prisma/schema.prisma",
|
||||||
migrations: {
|
migrations: {
|
||||||
path: "prisma/migrations",
|
path: "prisma/migrations",
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
url: process.env["DATABASE_URL"],
|
url: process.env["DATABASE_URL"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,33 @@
|
||||||
{
|
{
|
||||||
|
"include": ["packages/**/*.ts", "packages/**/*.json"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"outDir": "./dist",
|
||||||
"module": "commonjs",
|
"rootDir": "./packages",
|
||||||
"lib": [ "ES2021", "DOM", "DOM.Iterable" ],
|
|
||||||
"moduleResolution": "node",
|
"resolveJsonModule": true,
|
||||||
"outDir": "dist",
|
"module": "esnext",
|
||||||
|
"target": "es2023",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"composite": true, // For incremental build
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": [],
|
||||||
|
|
||||||
|
// Other Outputs
|
||||||
|
"declaration": true, // .d.ts
|
||||||
|
"declarationMap": true, // source map
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
// Stricter Typechecking Options
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"exactOptionalPropertyTypes": false,
|
"noUncheckedIndexedAccess": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"exactOptionalPropertyTypes": true
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
},
|
||||||
"include": ["./packages/**/*"]
|
"buildOptions": {
|
||||||
|
"incremental": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue