UNPKG

tts-extractor

Version:

A text-to-speech extractor for discord-player

137 lines (116 loc) 4.5 kB
import assertInputTypes from "./assertInputTypes"; import splitLongText from "./splitLongText"; interface Option { lang?: string; slow?: boolean; host?: string; timeout?: number; } /** * Get "Google TTS" audio base64 text * * @param {string} text length should be less than 200 characters * @param {object?} option * @param {string?} option.lang default is "en" * @param {boolean?} option.slow default is false * @param {string?} option.host default is "https://translate.google.com" * @param {number?} option.timeout default is 10000 (ms) * @returns {Promise<string>} url */ export const getAudioBase64 = async ( text: string, { lang = "en", slow = false, host = "https://translate.google.com", timeout = 10000 }: Option = {}, ): Promise<string> => { assertInputTypes(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(); // Parse the response without using eval let result; try { const parsedData = JSON.parse(data.slice(5)); result = parsedData[0][2]; } catch (e) { throw new Error(`parse response failed:\n${data}`); } // Check the result. The result will be null if given the lang doesn't exist if (!result) throw new Error(`lang "${lang}" might not exist`); // Continue parsing the result without eval try { const parsedResult = JSON.parse(result); result = parsedResult[0]; } catch (e) { throw new Error(`parse response failed:\n${data}`); } return result; }; interface LongTextOption extends Option { splitPunct?: string; } /** * @typedef {object} Result * @property {string} shortText * @property {string} base64 */ /** * Split the long text into multiple short text and generate audio base64 list * * @param {string} text * @param {object?} option * @param {string?} option.lang default is "en" * @param {boolean?} option.slow default is false * @param {string?} option.host default is "https://translate.google.com" * @param {string?} option.splitPunct split punctuation * @param {number?} option.timeout default is 10000 (ms) * @return {Result[]} the list with short text and audio base64 */ export const getAllAudioBase64 = async ( text: string, { lang = "en", slow = false, host = "https://translate.google.com", splitPunct = "", timeout = 10000, }: LongTextOption = {}, ): Promise<{ shortText: string; base64: string }[]> => { assertInputTypes(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(text, { splitPunct }); const base64List = await Promise.all( shortTextList.map((shortText) => getAudioBase64(shortText, { lang, slow, host, timeout })), ); // put short text and base64 text in a list const result: { shortText: string; base64: string }[] = []; for (let i = 0; i < shortTextList.length; i++) { const shortText = shortTextList[i]; const base64 = base64List[i]; result.push({ shortText, base64 }); } return result; };