import { writeFile, mkdir, readFile } from "fs/promises"; import { dirname } from "path"; import { AudioResource, createAudioResource, StreamType, } from "@discordjs/voice"; import { Readable } from "stream"; import { createHash } from "node:crypto"; import { join } from "node:path"; import { existsSync } from "node:fs"; import { saferKorean } from "../utils/saferKorean"; export abstract class TTSModelBase { public ttsify(input: string): string { return saferKorean( input.replace( /:[^:]+:/g, (text: string): string => TTSModelBase.EMOJI_MAP[text] ?? "이모지", ), ); } public abstract createRequestId(text: string): RequestId; public abstract getVoiceBuffer(id: RequestId): Promise; public abstract getVoicePath(id: RequestId): string; /** * id로 부터 음성을 생성하여 캐시 파일에 저장합니다 * 생성된 음성을 반환합니다 */ public async createVoice(id: RequestId, audioPath?: string): Promise { 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 { audioPath ??= this.getVoicePath(id); if (existsSync(audioPath)) { const buffer = await readFile(audioPath); return buffer; } return this.createVoice(id, audioPath); } /** * id로 부터 메모리에 캐싱된 음성을 얻거나, 파일에 캐싱된 * 음성을 얻거나, 없는 경우 생성합니다 */ protected abstract cachedVoice: Map>; public async getMemcachedVoice(id: RequestId): Promise { 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 const EMOJI_MAP: { [key: string]: string } = { ":heart:": "하트", ":huck:": "헉헉!", ":star:": "초롱초롱!", }; export const AudioCachePath = join(process.cwd(), "cache", "audio"); export function bufferToAudioResource(buf: Buffer): AudioResource { const stream = Readable.from(buf); const resource = createAudioResource(stream, { inlineVolume: true, inputType: StreamType.Arbitrary, }); resource.volume?.setVolume(0.3); return resource; } export function hashAudioFile(audio: string, suffix: string = ""): string { return createHash("md5").update(audio).digest("hex") + suffix + ".mp3"; } export const MemCacheTTL = 60 * 60 * 1000; } export default TTSModelBase;