UNPKG

node-red-contrib-tts-ultimate

Version:

Transforms the text in speech and hear it using Sonos, Google Cast (Chromecast / Nest) or DLNA/UPnP players, or generate an audio file to be used with third parties nodes. Works with voices from Google (without credentials as well), Google TTS, ElevenLabs

116 lines (98 loc) 4 kB
const fs = require("fs"); const path = require("path"); const GoogleTranslate = require("../ttsultimate/lib/googletranslate"); const GOOGLE_TRANSLATE_MAX_CHARS = 200; const argValue = (name, fallback) => { const idx = process.argv.indexOf(name); if (idx === -1) return fallback; const value = process.argv[idx + 1]; if (!value || value.startsWith("--")) return fallback; return value; }; const flagEnabled = (name) => process.argv.includes(name); const help = () => { // eslint-disable-next-line no-console console.log(`Usage: node scripts/verify-googletranslate-split.js --voice it-IT --text "..." --out ./out.mp3 Options: --voice Voice code (default: it-IT) --text Text to synthesize (default: long sample text) --out Output file path (default: ./googletranslate-split.mp3) --slow Enable slow speaking --help Show help `); }; const stripId3v2 = (buffer) => { if (!Buffer.isBuffer(buffer) || buffer.length < 10) return buffer; if (buffer[0] !== 0x49 || buffer[1] !== 0x44 || buffer[2] !== 0x33) return buffer; // "ID3" const size = ((buffer[6] & 0x7f) << 21) | ((buffer[7] & 0x7f) << 14) | ((buffer[8] & 0x7f) << 7) | (buffer[9] & 0x7f); const tagEnd = 10 + size; if (tagEnd <= 10 || tagEnd >= buffer.length) return buffer; return buffer.subarray(tagEnd); }; const splitText = (text, maxLen) => { const chunks = []; let remaining = (text ?? "").toString().trim(); if (remaining === "") return chunks; const breakChars = ["\n", ".", "!", "?", ";", ":", ",", " "]; while (remaining.length > maxLen) { const window = remaining.slice(0, maxLen + 1); let breakAt = -1; for (const ch of breakChars) { const idx = window.lastIndexOf(ch); if (idx > breakAt) breakAt = idx; } if (breakAt <= 0) breakAt = maxLen; const cutAt = breakAt === maxLen ? maxLen : breakAt + 1; const chunk = remaining.slice(0, cutAt).trim(); if (chunk !== "") chunks.push(chunk); remaining = remaining.slice(cutAt).trimStart(); } if (remaining !== "") chunks.push(remaining); return chunks; }; const isLikelyMp3 = (buffer) => { if (!Buffer.isBuffer(buffer) || buffer.length < 3) return false; if (buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) return true; // "ID3" if (buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0) return true; // frame sync return false; }; const main = async () => { if (flagEnabled("--help")) { help(); process.exit(0); } const voice = argValue("--voice", "it-IT"); const slow = flagEnabled("--slow"); const outFile = argValue("--out", path.resolve(process.cwd(), "googletranslate-split.mp3")); const text = argValue( "--text", "Questo è un testo di esempio volutamente molto lungo per verificare lo split automatico in blocchi da 200 caratteri. " + "Il file MP3 risultante deve essere unico e riproducibile, anche se generato da più richieste consecutive al servizio di Google Translate TTS." ); const resolvedVoice = typeof voice === "string" && voice.includes("-") ? voice.split("-")[0] : voice; const chunks = splitText(text, GOOGLE_TRANSLATE_MAX_CHARS); // eslint-disable-next-line no-console console.log(`Text length: ${text.length} chars, chunks: ${chunks.length}, voice: ${resolvedVoice}, slow: ${slow}`); const buffers = []; for (let i = 0; i < chunks.length; i += 1) { const chunkBuffer = await GoogleTranslate.synthesize({ text: chunks[i], voice: resolvedVoice, slow }); buffers.push(i === 0 ? chunkBuffer : stripId3v2(chunkBuffer)); } const mp3 = Buffer.concat(buffers); if (!isLikelyMp3(mp3)) { throw new Error("Output does not look like an MP3 (missing ID3 header or frame sync)."); } fs.writeFileSync(outFile, mp3); // eslint-disable-next-line no-console console.log(`Wrote: ${outFile} (${mp3.length} bytes)`); }; main().catch((err) => { // eslint-disable-next-line no-console console.error(err); process.exit(1); });