discord-player-tts
Version:
A text-to-speech extractor for discord-player
226 lines (220 loc) • 7.39 kB
JavaScript
// src/TTSExtractor.ts
import {
BaseExtractor,
Track,
Util
} from "discord-player";
import { Readable } from "stream";
// src/google-tts-api/assertInputTypes.ts
var assertInputTypes = (text, lang, slow, host) => {
if (typeof text !== "string" || text.length === 0)
throw new TypeError("text should be a string");
if (typeof lang !== "string" || lang.length === 0)
throw new TypeError("lang should be a string");
if (typeof slow !== "boolean")
throw new TypeError("slow should be a boolean");
if (typeof host !== "string" || host.length === 0)
throw new TypeError("host should be a string");
};
var assertInputTypes_default = assertInputTypes;
// src/google-tts-api/splitLongText.ts
var SPACE_REGEX = "\\s\\uFEFF\\xA0";
var DEFAULT_PUNCTUATION_REGEX = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
var splitLongText = (text, { maxLength = 200, splitPunct = "" } = {}) => {
const isSpaceOrPunct = (s, i) => {
const regex = new RegExp("[" + SPACE_REGEX + DEFAULT_PUNCTUATION_REGEX + splitPunct + "]");
return regex.test(s.charAt(i));
};
const lastIndexOfSpaceOrPunct = (s, left, right) => {
for (let i = right; i >= left; i--)
if (isSpaceOrPunct(s, i)) return i;
return -1;
};
const result = [];
const addResult = (text2, start2, end) => {
result.push(text2.slice(start2, end + 1));
};
let start = 0;
for (; ; ) {
if (text.length - start <= maxLength) {
addResult(text, start, text.length - 1);
break;
}
let end = start + maxLength - 1;
if (isSpaceOrPunct(text, end) || isSpaceOrPunct(text, end + 1)) {
addResult(text, start, end);
start = end + 1;
continue;
}
end = lastIndexOfSpaceOrPunct(text, start, end);
if (end === -1) {
const str = text.slice(start, start + maxLength);
throw new Error(
`The word is too long to split into a short text:
${str} ...
Try the option "splitPunct" to split the text by punctuation.`
);
}
addResult(text, start, end);
start = end + 1;
}
return result;
};
var splitLongText_default = splitLongText;
// src/google-tts-api/getAudioBase64.ts
var getAudioBase64 = async (text, { lang = "en", slow = false, host = "https://translate.google.com", timeout = 1e4 } = {}) => {
assertInputTypes_default(text, lang, slow, host);
if (typeof timeout !== "number" || timeout <= 0)
throw new TypeError("timeout should be a positive number");
if (text.length > 200) {
throw new RangeError(
`text length (${text.length}) should be less than 200 characters. Try "getAllAudioBase64(text, [option])" for long text.`
);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const res = await fetch(`${host}/_/TranslateWebserverUi/data/batchexecute`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `f.req=${encodeURIComponent(
JSON.stringify([
[["jQ1olc", JSON.stringify([text, lang, slow ? true : null, "null"]), null, "generic"]]
])
)}`,
signal: controller.signal
}).finally(() => clearTimeout(timeoutId));
if (!res.ok)
throw new Error(`Request failed with status ${res.status}`);
const data = await res.text();
let result;
try {
const parsedData = JSON.parse(data.slice(5));
result = parsedData[0][2];
} catch (e) {
throw new Error(`parse response failed:
${data}`);
}
if (!result)
throw new Error(`lang "${lang}" might not exist`);
try {
const parsedResult = JSON.parse(result);
result = parsedResult[0];
} catch (e) {
throw new Error(`parse response failed:
${data}`);
}
return result;
};
var getAllAudioBase64 = async (text, {
lang = "en",
slow = false,
host = "https://translate.google.com",
splitPunct = "",
timeout = 1e4
} = {}) => {
assertInputTypes_default(text, lang, slow, host);
if (typeof splitPunct !== "string")
throw new TypeError("splitPunct should be a string");
if (typeof timeout !== "number" || timeout <= 0)
throw new TypeError("timeout should be a positive number");
const shortTextList = splitLongText_default(text, { splitPunct });
const base64List = await Promise.all(
shortTextList.map((shortText) => getAudioBase64(shortText, { lang, slow, host, timeout }))
);
const result = [];
for (let i = 0; i < shortTextList.length; i++) {
const shortText = shortTextList[i];
const base64 = base64List[i];
result.push({ shortText, base64 });
}
return result;
};
// src/TTSExtractor.ts
var _TTSExtractor = class _TTSExtractor extends BaseExtractor {
async activate() {
this.protocols = ["tts"];
_TTSExtractor.instance = this;
}
async deactivate() {
this.protocols = [];
_TTSExtractor.instance = null;
}
async validate(query, type) {
return typeof query === "string" && (type === "tts" || new URL(query).protocol === "tts:");
}
async handle(query, context) {
if (!context.protocol || context.protocol !== "tts") {
this.debug("Invalid protocol, skipping...");
return this.createResponse(null, []);
}
const trackInfo = {
title: "TTS Query",
author: "google-tts-api",
duration: 0,
thumbnail: "https://upload.wikimedia.org/wikipedia/commons/2/2a/ITunes_12.2_logo.png",
description: query,
requestedBy: null,
raw: { query, type: "tts" }
};
const track = new Track(this.context.player, {
title: trackInfo.title,
duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)),
url: `tts://${trackInfo.title}`,
description: trackInfo.description,
thumbnail: trackInfo.thumbnail,
views: 0,
author: trackInfo.author,
requestedBy: context.requestedBy,
source: "arbitrary",
metadata: trackInfo,
query,
// @ts-expect-error queryType is not in the type definition
queryType: "tts",
raw: trackInfo.raw,
async requestMetadata() {
return trackInfo;
}
});
track.extractor = this;
return this.createResponse(null, [track]);
}
async stream(track) {
const raw = track.raw;
const audioBuffer = await this.getCombinedAudioBuffer(raw.query);
const audioStream = Readable.from(audioBuffer);
return audioStream;
}
async getRelatedTracks() {
return this.createResponse(null, []);
}
async getCombinedAudioBuffer(inputText) {
const splitLongWords = (textInput) => {
const maxWordLength = 200;
return textInput.split(/\s+/).flatMap((word) => {
if (word.length > maxWordLength) {
const chunks = [];
for (let i = 0; i < word.length; i += maxWordLength)
chunks.push(word.slice(i, i + maxWordLength));
return chunks;
}
return word;
}).join(" ");
};
const sanitizedText = splitLongWords(inputText);
const audioBase64Parts = await getAllAudioBase64(sanitizedText, {
lang: this.options.language || "en",
slow: this.options.slow ?? false,
splitPunct: ",.?!;:"
});
const audioBuffers = audioBase64Parts.map((part) => Buffer.from(part.base64, "base64"));
return Buffer.concat(audioBuffers);
}
};
_TTSExtractor.identifier = "com.itsmaat.discord-player.tts-extractor";
_TTSExtractor.instance = null;
var TTSExtractor = _TTSExtractor;
export {
TTSExtractor
};