yaejunyang/packages/tts/typecast.ts
2026-02-09 18:31:56 +00:00

176 lines
6.6 KiB
TypeScript

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<TTSTypecastModel.RequestId> {
protected cachedVoice: Map<String, Promise<Buffer>>
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<ArrayBuffer> {
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;