import { join } from "path"; import { TYPECAST_TOKENS } from "../env"; import fetch from "../utils/fetch"; import TTSModelBase from "."; import CallingNumberKorean from "../utils/callingNumberKorean"; import IntegerKorean from "../utils/integerKorean"; import FloatKorean from "../utils/floatKorean"; import { readFileSync, writeFileSync } from "fs"; import { cwd, env } from "process"; export class TTSTypecastModel extends TTSModelBase { protected cachedVoice: Map> private lastUseApiKeyPath: string constructor() { super() this.cachedVoice = new Map(); this.lastUseApiKeyPath = join(cwd(), "cache", "typecast", "lastUseApiToken"); } ttsify(input: string): string { return super.ttsify( input .replace(/\.+$/, "") .replace(/\.\.+/g, "") .replace(/\.[ \t]/g, " ") .replace(/^[\?\!\'\"]+$/, (total)=>( [...total].map(element => TTSTypecastModel.IsolatedSymbolMap[ element as keyof typeof TTSTypecastModel.IsolatedSymbolMap ]).join("") )) .replace(/\`\`\`.+?\`\`\`/g, "코드블럭") .replace(/https\S+/g, "링크") .replace(/ㄴㄴ/g, "노노") .replace(/ㅇㅋ/g, "오키") .replace(/ㅜㅜ/g, "눙물") .replace(/빵/g, "빵 크크") .replace(/[\?]+ *ㄴ/g, "물음표ㄴ") .replace(/(\d+)[ \t\n]*([개살])/g, (_, num: string, postfix: string)=>{ const intNum = parseInt(num) if (CallingNumberKorean.canConvert(intNum)) { return CallingNumberKorean.convert(intNum) + postfix; } else { return IntegerKorean.convertFromString(num) + postfix; } }) .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") { return ( "버전" + FloatKorean.convert(num) + (TTSTypecastModel.VersionPostfix[ postfix as keyof typeof TTSTypecastModel.VersionPostfix ] ?? "") ); } else { return FloatKorean.convert(num) + postfix; } }) .replace(/[\%\^\&\*\#\@\.\-\+\_\=\/\\♡\$]/g, (t) => ( TTSTypecastModel.SymbolMap[t as keyof typeof TTSTypecastModel.SymbolMap] )) .replace(/\?+/g, "?") .replace(/\!+/g, "!") ) } private async getTypecastResponse(apiKey: string, voiceId: TTSTypecastModel.RequestId) { const payload = { text: voiceId.text, model: "ssfm-v21", voice_id: voiceId.voiceId, language: "kor", prompt: { emotion_preset: "happy", // Options: normal, happy, sad, angry, tonemid, toneup 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, { method: "POST", headers: { "X-API-KEY": apiKey, "Content-Type": "application/json" }, body: JSON.stringify(payload) }); } async getVoiceBuffer(voiceId: TTSTypecastModel.RequestId): Promise { let response: Response | undefined; for (let i = 0; i < TYPECAST_TOKENS.length; i++) { response = await this.getTypecastResponse(readFileSync(this.lastUseApiKeyPath, "utf-8"), voiceId) as Response; if (response.ok) return await response.arrayBuffer();; if (response.status === 402) { writeFileSync(this.lastUseApiKeyPath, TYPECAST_TOKENS[i]); } else { 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, }; } } export namespace TTSTypecastModel { export const IsolatedSymbolMap = { "?": "물음표", "!": "느낌표", "'": "쿼트", "\"": "더블쿼트", } export const SymbolMap = { "%": "퍼센트", "$": "달러싸인", "^": "캐럿", "&": "엠퍼센드", "*": "스타", "#": "해시", "@": "엣", ".": "쩜", "-": "마이너스", "+": "플러스", "_": "언더바", "=": "이퀄", "/": "슬래쉬", "\\": "역슬래쉬", "♡": "하투 ", }; export const VersionPostfix = { "a": "알파", "b": "베타", }; export const instance = new TTSTypecastModel(); export type RequestId = { text: string, voiceId: string }; export const TypecastAudioCachePath = join(TTSModelBase.AudioCachePath, "typecast"); export const TypecastApiUrl = "https://api.typecast.ai/v1/text-to-speech"; export const DefaultVoiceId = "tc_6731b292d944a485bc406efb"; } export default TTSTypecastModel;