 | |  |  | 语音合成核心模块AIWROK 单文件版
- /**
- * ============================================================================
- * AIWROK TTS CORE - 语音合成核心模块(AIWROK 单文件版)
- * ============================================================================
- */
- // ==================== 导入 Java 类(AIWROK 内置) ====================
- importClass(Packages.java.lang.Thread);
- importClass(Packages.java.io.File);
- importClass(Packages.java.io.FileOutputStream);
- importClass(Packages.java.io.BufferedOutputStream);
- importClass(Packages.java.security.MessageDigest);
- // 注意:Array 和 Byte 不做 importClass,会和 JS 全局对象冲突
- // 代码里直接用完整类名:java.lang.reflect.Array / java.lang.Byte
- importClass(Packages.okhttp3.OkHttpClient);
- importClass(Packages.okhttp3.Request);
- importClass(Packages.okhttp3.RequestBody);
- importClass(Packages.okhttp3.WebSocketListener);
- importClass(Packages.okio.ByteString);
- importClass(Packages.okio.Buffer);
- importClass(Packages.java.util.concurrent.TimeUnit);
- // ==================== 音色列表 ====================
- var EDGE_VOICES = [
- { id: "zh-CN-XiaoxiaoNeural", name: "晓晓 (甜美女声)" },
- { id: "zh-CN-XiaoyiNeural", name: "晓伊 (软萌少女)" },
- { id: "zh-CN-XiaohanNeural", name: "晓涵 (知性御姐)" },
- { id: "zh-CN-XiaomengNeural", name: "晓梦 (温柔治愈)" },
- { id: "zh-CN-XiaomoNeural", name: "晓墨 (成熟稳重)" },
- { id: "zh-CN-XiaoxuanNeural", name: "晓萱 (活泼可爱)" },
- { id: "zh-CN-XiaoyanNeural", name: "晓颜 (新闻女主播)" },
- { id: "zh-CN-XiaochenNeural", name: "晓辰 (温暖女声)" },
- { id: "zh-CN-XiaoshuangNeural", name: "晓双 (儿童女声)" },
- { id: "zh-CN-XiaoruiNeural", name: "晓睿 (老年女声)" },
- { id: "zh-CN-YunxiNeural", name: "云希 (年轻男声)" },
- { id: "zh-CN-YunjianNeural", name: "云健 (成熟男声)" },
- { id: "zh-CN-YunyangNeural", name: "云扬 (新闻播音)" },
- { id: "zh-CN-YunxiaNeural", name: "云夏 (少年男声)" },
- { id: "zh-CN-YunyeNeural", name: "云野 (故事讲述)" },
- { id: "zh-CN-YunfengNeural", name: "云枫 (老年男声)" },
- { id: "zh-TW-HsiaoChenNeural", name: "曉臻 (台湾女声)" },
- { id: "zh-TW-YunJheNeural", name: "雲哲 (台湾男声)" },
- { id: "zh-HK-HiuMaanNeural", name: "曉曼 (粤语女声)" },
- { id: "zh-HK-WanLungNeural", name: "雲龍 (粤语男声)" }
- ];
- var DOUBAO_VOICES = [
- { id: "zh_female_taozi_conversation_v4_wvae_bigtts", name: "桃子 (温柔女声)" },
- { id: "zh_male_wennuanahu_radar_v4_wvae_bigtts", name: "温暖 (男声)" },
- { id: "zh_female_qingxinahu_radar_v4_wvae_bigtts", name: "清新 (女声)" },
- { id: "zh_male_zhiyu_radar_v5_wvae_bigtts", name: "治愈 (男声)" },
- { id: "zh_female_wanwanxiaohe_moon_bigtts", name: "弯弯小贺 (女童)" },
- { id: "zh_female_linjiaxiaoyu_moon_bigtts", name: "邻家小语 (少女)" }
- ];
- var DEFAULT_EDGE_VOICE = "zh-CN-XiaoxiaoNeural";
- var DEFAULT_DOUBAO_VOICE = "zh_female_taozi_conversation_v4_wvae_bigtts";
- // ==================== 通用工具函数 ====================
- /**
- * 生成 UUID v4(Rhino ES5 兼容)
- */
- function uuid() {
- var chars = '0123456789abcdef';
- var s = [];
- for (var i = 0; i < 36; i++) s[i] = chars[Math.floor(Math.random() * 16)];
- s[14] = '4';
- s[19] = chars[(parseInt(s[19], 16) & 0x3) | 0x8];
- s[8] = s[13] = s[18] = s[23] = '-';
- return s.join('');
- }
- /** XML 转义 */
- function escapeXml(str) {
- return String(str)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- }
- /**
- * 把 JS 的 ArrayBuffer / Uint8Array / number[] 转成 Java byte[]
- * Rhino 里 ByteString.of() 和 Java IO 都需要原生 byte[]
- */
- function toJavaBytes(data) {
- var view;
- if (data instanceof ArrayBuffer) {
- view = new Uint8Array(data);
- } else if (data instanceof Uint8Array) {
- view = data;
- } else if (data && typeof data.length === 'number') {
- view = new Uint8Array(data.length);
- for (var i2 = 0; i2 < data.length; i2++) view[i2] = data[i2];
- } else {
- return java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 0);
- }
- var jBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, view.length);
- for (var k = 0; k < view.length; k++) {
- var v = view[k] & 0xff;
- jBytes[k] = (v > 127) ? (v - 256) : v;
- }
- return jBytes;
- }
- /**
- * 把 Uint8Array / number[] / ArrayBuffer 写入文件
- * @param {string} path
- * @param {Uint8Array|number[]|ArrayBuffer} data
- * @return {boolean}
- */
- function writeBinaryFile(path, data) {
- try {
- var jBytes = toJavaBytes(data);
- if (!jBytes || jBytes.length === 0) return false;
- // 确保父目录存在
- var f = new java.io.File(path);
- var parent = f.getParentFile();
- if (parent && !parent.exists()) parent.mkdirs();
- var fos = new java.io.FileOutputStream(f);
- var out = new java.io.BufferedOutputStream(fos);
- out.write(jBytes);
- out.flush();
- out.close();
- fos.close();
- return true;
- } catch (e) {
- print.log("[writeBinaryFile] 失败: " + e);
- return false;
- }
- }
- /**
- * 简易 sleep(不抛异常)
- */
- function sleep(ms) {
- try { java.lang.Thread.sleep(ms); } catch (e) {}
- }
- /**
- * 字符串左侧补齐(Rhino 不支持 padStart)
- */
- function padStart(s, len, ch) {
- s = String(s);
- while (s.length < len) s = ch + s;
- return s;
- }
- // ==================== Protobuf 编解码(豆包用) ====================
- function encodeVarint(v) {
- var out = [];
- v = v >>> 0;
- while (v > 0x7f) {
- out.push((v & 0x7f) | 0x80);
- v = v >>> 7;
- }
- out.push(v & 0x7f);
- return out;
- }
- function encodeBytesField(fieldNum, data) {
- var bytes;
- if (typeof data === 'string') {
- // UTF-8 编码(不能用 unescape,中文会乱码)
- var u8 = unescape(encodeURIComponent(data));
- bytes = [];
- for (var i = 0; i < u8.length; i++) bytes.push(u8.charCodeAt(i) & 0xff);
- } else if (data && data.length !== undefined) {
- bytes = [];
- for (var j = 0; j < data.length; j++) bytes.push(data[j] & 0xff);
- } else {
- bytes = [];
- }
- var tag = (fieldNum << 3) | 2;
- return encodeVarint(tag).concat(encodeVarint(bytes.length), bytes);
- }
- /**
- * 构造完整的 VoiceGenie 请求帧,返回 ArrayBuffer
- */
- function buildRequestFrame(event, payload, sessionId) {
- var frame = [];
- frame = frame.concat(encodeBytesField(2, 'GOqQpfo1fO7slHv8'));
- frame = frame.concat(encodeBytesField(3, 'VoiceGenie'));
- frame = frame.concat(encodeBytesField(5, event));
- frame = frame.concat(encodeBytesField(6, payload));
- if (sessionId) {
- frame = frame.concat(encodeBytesField(8, sessionId));
- }
- // number[] → ArrayBuffer
- var ab = new ArrayBuffer(frame.length);
- var v = new Uint8Array(ab);
- for (var i = 0; i < frame.length; i++) v[i] = frame[i];
- return ab;
- }
- /**
- * 解析服务器返回的 protobuf 帧
- */
- function parseResponseFrame(buf) {
- var a = (buf instanceof Uint8Array) ? buf : new Uint8Array(buf);
- var fields = [];
- var i = 0;
- function readVarint() {
- var v = 0, s = 0;
- while (i < a.length) {
- var b = a[i++];
- v |= (b & 0x7f) << s;
- if (!(b & 0x80)) return v >>> 0;
- s += 7;
- if (s > 35) break;
- }
- return v >>> 0;
- }
- while (i < a.length) {
- var tag;
- try { tag = readVarint(); } catch (e) { break; }
- if (tag === 0) break;
- var fieldNum = tag >> 3;
- var wireType = tag & 7;
- var field = { fieldNum: fieldNum, wireType: wireType };
- if (wireType === 0) {
- field.value = readVarint();
- } else if (wireType === 2) {
- var len = readVarint();
- // 兼容 Rhino Uint8Array.subarray/slice
- var sliceArr = [];
- for (var kk = 0; kk < len; kk++) sliceArr.push(a[i + kk]);
- field.data = sliceArr; // number[]
- i += len;
- if (len <= 8000) {
- try {
- // 转 Java byte[] 再 UTF-8 解码(中文安全)
- var jb = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, len);
- for (var m = 0; m < len; m++) {
- var bv = a[i - len + m] & 0xff;
- jb[m] = (bv > 127) ? (bv - 256) : bv;
- }
- field.asString = String(new java.lang.String(jb, "UTF-8"));
- } catch (e) {}
- }
- } else if (wireType === 5) {
- i += 4;
- } else if (wireType === 1) {
- i += 8;
- } else {
- break;
- }
- fields.push(field);
- }
- var result = { eventType: null, payload: null, audioData: null, seq: -1, sessionId: null };
- for (var f = 0; f < fields.length; f++) {
- var fld = fields[f];
- if (fld.fieldNum === 1) result.sessionId = fld.asString || null;
- if (fld.fieldNum === 4) result.eventType = fld.asString || null;
- if (fld.fieldNum === 7) result.payload = fld.data || null;
- if (fld.fieldNum === 8) result.audioData = fld.data; // number[]
- if (fld.fieldNum === 9) result.seq = fld.value;
- }
- return result;
- }
- // ==================== 豆包 Cookie / UID 工具 ====================
- function cookieObjToString(cookies) {
- if (typeof cookies === 'string') return cookies;
- if (cookies && typeof cookies === 'object') {
- // ES5 没有 Object.entries
- var parts = [];
- for (var k in cookies) {
- if (cookies.hasOwnProperty(k)) {
- parts.push(k + '=' + cookies[k]);
- }
- }
- return parts.join('; ');
- }
- return '';
- }
- function getCookieFromStr(cookieStr, name) {
- if (!cookieStr) return null;
- var m = cookieStr.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]+)'));
- return m ? m[1] : null;
- }
- function getUid(cookieStr) {
- var uidCookie = getCookieFromStr(cookieStr, 'uid_tt') || getCookieFromStr(cookieStr, 'uid');
- if (uidCookie) {
- try {
- // 用 Java BASE64 解码(Rhino 没有 atob)
- var decoded = String(java.util.Base64.getDecoder().decode(uidCookie));
- if (/^\d+$/.test(decoded)) return decoded;
- } catch (e) {}
- if (/^\d+$/.test(uidCookie)) return uidCookie;
- }
- return String(Date.now() % 1000000000000);
- }
- function buildDoubaoWsUrl(cookieStr) {
- var deviceId = getCookieFromStr(cookieStr, 'device_id') ||
- getCookieFromStr(cookieStr, 's_v_web_id') ||
- (String(Date.now()) + padStart(String(Math.floor(Math.random() * 1e6)), 6, '0'));
- var webId = getCookieFromStr(cookieStr, 'web_id') ||
- getCookieFromStr(cookieStr, 's_v_web_id') ||
- deviceId;
- var params = [
- 'api_app_key=GOqQpfo1fO7slHv8',
- 'namespace=VoiceGenie',
- 'version_code=20800',
- 'language=zh',
- 'device_platform=web',
- 'pkg_type=release_version',
- 'pc_version=3.30.5',
- 'region=CN',
- 'sys_region=CN',
- 'samantha_web=1',
- 'use-olympus-account=1',
- 'doubao_device_platform=web',
- 'aid=497858',
- 'real_aid=497858',
- 'device_id=' + deviceId,
- 'doubao_pc_version=3.30.5',
- 'web_id=' + webId,
- 'tea_uuid=' + webId,
- 'web_platform=browser',
- 'web_tab_id=' + uuid()
- ];
- return 'wss://frontier-audio-web-ws.doubao.com/api/v2/sami/voicegenie?' + params.join('&');
- }
- function buildDoubaoSessionPayload(opts) {
- var replyUniqueKey = uuid();
- var queryLocalMessageId = uuid();
- var uid = getUid(opts.cookieStr);
- var payload = {
- business: 1,
- conversation_id: opts.conversationId,
- chat: {
- bot_id: '7338286299411103781',
- conversation_id: opts.conversationId,
- question_id: opts.questionId,
- message_id: opts.messageId,
- uid: uid,
- new_conversation: false,
- chat_next: '1',
- enable_toast_reading: 'true',
- extra: { chat_next: '1', reply_unique_key: replyUniqueKey, query_local_message_id: queryLocalMessageId },
- reply_unique_key: replyUniqueKey,
- section_id: opts.sectionId,
- local_message_id: uuid(),
- query_local_message_id: queryLocalMessageId
- },
- request_type: 4,
- enable_text_reading: true,
- interrupt_type: 0,
- query_mode: 2,
- tts: {
- speaker: opts.speaker,
- audio_config: opts.audioConfig,
- extra: {
- chat_next: '1',
- query_local_message_id: queryLocalMessageId,
- post_process: { pitch: opts.pitch || 0, speech_rate: opts.speechRate || 1.0 }
- }
- },
- extra: {
- enable_toast_reading: 'true',
- chat_next: '1',
- uid: uid,
- section_id: opts.sectionId,
- reply_unique_key: replyUniqueKey,
- query_local_message_id: queryLocalMessageId,
- enable_text_simple_report: true,
- enable_section_report: true,
- enable_card_simple_report: true,
- enable_image_asr_ctx: true,
- disable_markdown_filter: true,
- enable_asr_audio_context: false,
- is_server_error_retry: false
- }
- };
- return JSON.stringify(payload);
- }
- // ==================== Edge TTS 核心(1:1 对应 Python rany2/edge-tts 官方算法) ====================
- var EDGE_TTS_WS_URL = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
- var EDGE_TTS_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
- // 完全对齐官方 constants.py CHROMIUM_FULL_VERSION
- var EDGE_CHROMIUM_FULL_VERSION = '143.0.3650.75';
- var EDGE_CHROMIUM_MAJOR_VERSION = '143';
- var EDGE_TTS_GEC_VERSION = '1-' + EDGE_CHROMIUM_FULL_VERSION;
- var EDGE_WIN_EPOCH_SECONDS = 11644473600;
- /**
- * 全局 DRM 时钟偏差(单位秒,正数=服务器比本地快)
- * 官方:DRM.clock_skew_seconds,403失败时从服务器 Date 头校准后自动重试
- */
- var EDGE_CLOCK_SKEW_SECONDS = 0.0;
- /**
- * 解析 RFC 2616 GMT 日期(RFC_1123_DATE_TIME 格式)
- * 优先使用 Java DateTimeFormatter,避免自己正则解析出错。
- *
- * @param {string|null|undefined} dateStr 例如 "Sat, 08 Aug 2026 07:20:58 GMT"
- * @returns {number|null} UTC 秒数(float),失败返回 null
- */
- function parseRfc2616Date(dateStr) {
- if (!dateStr) return null;
- var s = String(dateStr).trim();
- try {
- // === 方案1:Java 8+ java.time(安卓 7.0+ 支持,AIWROK 运行环境一般都有) ===
- var formatter = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
- var temporal = formatter.parse(s);
- var zdt = java.time.ZonedDateTime.from(temporal);
- var epochSec = zdt.toEpochSecond(); // long
- // 注意:RFC 2616 日期不提供毫秒,只到秒
- return parseFloat(String(epochSec));
- } catch (e1) {
- try {
- // === 方案2:老安卓 SimpleDateFormat ===
- var sdf = new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", java.util.Locale.US);
- sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
- var d = sdf.parse(s);
- return parseFloat(String(d.getTime())) / 1000.0;
- } catch (e2) {
- // === 方案3:最后兜底 Rhino JS 原生 Date.parse ===
- try {
- var fb = Date.parse(s);
- if (!isNaN(fb)) return fb / 1000.0;
- } catch (e3) {}
- return null;
- }
- }
- }
- /**
- * 生成 MUID(32位随机大写HEX)
- * 官方 DRM.generate_muid → headers_with_muid 塞到 Cookie 里
- */
- function generateEdgeMuid() {
- var hex = '0123456789ABCDEF';
- var sb = '';
- for (var i = 0; i < 32; i++) {
- sb += hex.charAt(Math.floor(Math.random() * 16));
- }
- return sb;
- }
- /**
- * 生成 Sec-MS-GEC 签名(严格对齐官方 drm.py DRM.generate_sec_ms_gec + clock_skew)
- *
- * 官方 Python 流程:
- * ticks = dt.now(tz.utc).timestamp() + DRM.clock_skew_seconds
- * ticks += WIN_EPOCH
- * ticks -= ticks % 300
- * ticks *= 1e7
- * str = f"{ticks:.0f}{TOKEN}"
- * return sha256(str.encode("ascii")).hexdigest().upper()
- */
- var _EDGE_GEC_LOG_ONCE = false; // 只打印一次避免刷屏
- function generateSecMsGec() {
- try {
- var unixSecFloat = (java.lang.System.currentTimeMillis() / 1000.0) + EDGE_CLOCK_SKEW_SECONDS;
- var ticks = unixSecFloat + EDGE_WIN_EPOCH_SECONDS;
- // 对负数取模对齐 Python:Python % 永远非负,Rhino/JS 可能返回负数 → 手动归一化
- var mod = ticks % 300;
- if (mod < 0) mod += 300;
- ticks = ticks - mod;
- ticks = ticks * 10000000.0;
- var ticksLong = java.lang.Math.round(ticks);
- var ticksStr = String(ticksLong);
- var src = ticksStr + EDGE_TTS_TOKEN;
- var md = MessageDigest.getInstance('SHA-256');
- var srcBytes = new java.lang.String(src).getBytes('US-ASCII');
- var hash = md.digest(srcBytes);
- var sb = new java.lang.StringBuilder(hash.length * 2);
- for (var i = 0; i < hash.length; i++) {
- var h = hash[i] & 0xff;
- // ============================================================
- // 修复 Rhino 平台大坑:
- // java.lang.Integer.toHexString() 返回的是 java.lang.String(不是 JS String!)
- // → java.lang.String 的 .length 是 METHOD/FUNCTION,不是 JS 数值属性 length
- // → 旧代码:if (s.length < 2) → NaN < 2 → 永远 FALSE!
- // → 结果:只要 byte < 0x10(高位为 0),补零就被跳过!
- // → 每少一个零就错位 4 bit,导致整个 GEC 和官方差好几个字符 → 微软 403
- // ============================================================
- var hexJava = java.lang.Integer.toHexString(h);
- // ① 强转为 JS 原生 String(此时 .length 就是数值属性)
- var hex = String(hexJava);
- // ② 也可以用 Java 方法 .length() 双重保险(任一方式都可以)
- if (hex.length < 2) sb.append('0');
- sb.append(hex);
- }
- var gec = String(sb.toString()).toUpperCase();
- // ===== DEBUG:每次生成 GEC 时,只在第一次 / 403 前后打印关键参数 =====
- // 用户可复制下面这行到 Python gec_verify.py 做 1:1 对撞
- if (!_EDGE_GEC_LOG_ONCE || EDGE_CLOCK_SKEW_SECONDS !== 0.0) {
- var dbgLine = '[Edge TTS][GEC] ticks_input=' + java.lang.String.format("%.6f", unixSecFloat)
- + ' skew=' + EDGE_CLOCK_SKEW_SECONDS.toFixed(4)
- + ' mod300=' + mod.toFixed(6)
- + ' ticksLong=' + ticksLong
- + ' ticksStr_len=' + ticksStr.length
- + ' src_preview=' + ticksStr.substring(0, Math.min(12, ticksStr.length)) + '...'
- + ' gec=' + gec;
- print.log(dbgLine);
- _EDGE_GEC_LOG_ONCE = true;
- }
- return gec;
- } catch (e) {
- print.log('[Edge TTS] 生成 Sec-MS-GEC 失败: ' + e);
- return null;
- }
- }
- function buildSSML(text, voice, rate, volume, pitch) {
- return "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' " +
- "xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='zh-CN'>" +
- "<voice name='" + voice + "'>" +
- "<prosody rate='" + rate + "' volume='" + volume + "' pitch='" + pitch + "'>" +
- escapeXml(text) +
- "</prosody></voice></speak>";
- }
- /**
- * Edge TTS 单次握手尝试(内部函数,供 generateEdge 做自动重试)
- * @returns {Object} { ok, code, message, serverDate, output }
- */
- function tryEdgeOnce(sessionOpts) {
- var text = sessionOpts.text;
- var voice = sessionOpts.voice;
- var rate = sessionOpts.rate;
- var volume = sessionOpts.volume;
- var pitch = sessionOpts.pitch;
- var output = sessionOpts.output;
- var requestId = uuid().replace(/-/g, '');
- var gec = generateSecMsGec();
- var wsUrl = EDGE_TTS_WS_URL
- + '?TrustedClientToken=' + EDGE_TTS_TOKEN
- + '&ConnectionId=' + requestId;
- if (gec) {
- wsUrl += '&Sec-MS-GEC=' + gec
- + '&Sec-MS-GEC-Version=' + EDGE_TTS_GEC_VERSION;
- }
- var state = {
- connected: false,
- closed: false,
- err: null,
- httpCode: 0,
- serverDate: null,
- audioChunks: [],
- totalLen: 0,
- _loggedFirstFrame: false
- };
- var listener = new WebSocketListener({
- onOpen: function (ws, response) {
- state.connected = true;
- var code = 0;
- try { code = response ? response.code() : 0; } catch(e){}
- print.log('[Edge TTS][WS] ✅ onOpen 已触发,HTTP code=' + code + ' (期望 101 Switching Protocols)');
- try {
- var xts = (function isoZ() {
- var d = new Date();
- var pad = function (n, w) { n = String(n); while (n.length < w) n = '0' + n; return n; };
- return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1, 2) + '-' + pad(d.getUTCDate(), 2)
- + 'T' + pad(d.getUTCHours(), 2) + ':' + pad(d.getUTCMinutes(), 2) + ':' + pad(d.getUTCSeconds(), 2) + '.000Z';
- })();
- var configMsg = JSON.stringify({
- context: {
- synthesis: {
- audio: {
- metadataoptions: { sentenceBoundaryEnabled: false, wordBoundaryEnabled: false },
- outputFormat: 'audio-24khz-48kbitrate-mono-mp3'
- }
- }
- }
- });
- ws.send('X-Timestamp:' + xts + '\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n' + configMsg);
- var ssml = buildSSML(text, voice, rate, volume, pitch);
- ws.send('X-RequestId:' + requestId + '\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:' + xts + '\r\nPath:ssml\r\n\r\n' + ssml);
- } catch (e) {
- state.err = '发送失败: ' + e;
- print.log('[Edge TTS][WS] onOpen 内部发送失败: ' + e);
- }
- },
- onMessage: function (ws, message) {
- try {
- // ========== 1. 兼容 Rhino/OkHttp 多种消息类型 ==========
- if (message === null || message === undefined) return;
- // ----------- 文本消息:静默忽略(TEXT 帧是 turn.start / response / audio.metadata / turn.end 等控制消息) -----------
- var isJavaString = false;
- try {
- if (typeof message === 'string') {
- isJavaString = true;
- } else if (message && message.getClass) {
- var cls = message.getClass();
- if (cls === java.lang.String.class) isJavaString = true;
- // 再次兜底:如果是 okhttp 传过来的 String(比如某些版本会用
- // java.lang.CharSequence impl),调用 toString() 之后再检查
- // 是否全是可打印字符也可以 → 用 instanceof 更稳
- else if (java.lang.String.class.isInstance(message)) isJavaString = true;
- }
- } catch (e_ignore) {}
- if (isJavaString) return;
- // 二进制消息:可能是 okhttp3 ByteString (有 toByteArray()),可能是原生 byte[],可能是 Buffer/ByteString 其它
- var jBytes = null;
- try {
- if (typeof message.toByteArray === 'function') {
- jBytes = message.toByteArray();
- } else if (typeof message.length === 'number') {
- // 可能是 JS ArrayBuffer / Uint8Array? AIWROK Rhino 里 byte[] 也有 length 属性
- var maybeLen = message.length;
- if (maybeLen === 0 || (maybeLen > 0 && typeof message[0] === 'number')) {
- // JS Uint8Array 风格,转 Java byte[]
- jBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, maybeLen);
- for (var _k = 0; _k < maybeLen; _k++) {
- var _bv = message[_k] & 0xff;
- jBytes[_k] = (_bv > 127) ? (_bv - 256) : _bv;
- }
- } else if (maybeLen > 0 && message.getClass && message.getClass().isArray()) {
- // 原生 Java byte[]
- jBytes = message;
- }
- }
- } catch (convErr) {
- // 转换失败通常是文本控制帧,静默忽略即可
- return;
- }
- if (!jBytes) {
- // 非二进制消息(文本/其他控制帧),静默忽略
- return;
- }
- var len = jBytes.length;
- if (len < 2) {
- print.log('[Edge TTS][bin] frame too small, len=' + len);
- return;
- }
- // ========== 2. 按官方公式:header_length = 前 2 bytes big-endian ==========
- var headerLength = ((jBytes[0] & 0xff) << 8) | (jBytes[1] & 0xff);
- // 官方 get_headers_and_data: data[header_length+2:] (这里的 data 就是整个 jBytes)
- // 所以 header_length 必须 <= len - 2(后面至少留 2 字节给 start index)
- if (headerLength < 0 || (headerLength + 2) > len) {
- print.log('[Edge TTS][bin] invalid: headerLength=' + headerLength + ' totalLen=' + len);
- return;
- }
- var audioStart = headerLength + 2; // 官方 1:1
- var audioLen = len - audioStart; // 官方 1:1
- // ========== 3. 解析 header(官方:line.split(b":", 1),不 strip value) ==========
- var pathVal = null;
- var contentTypeVal = null;
- try {
- // jBytes[2 : 2 + headerLength] = header bytes
- var headerArr = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, headerLength);
- for (var _hi = 0; _hi < headerLength; _hi++) headerArr[_hi] = jBytes[2 + _hi];
- var headerStr = String(new java.lang.String(headerArr, 'US-ASCII'));
- var lines = headerStr.split('\r\n');
- for (var _li = 0; _li < lines.length; _li++) {
- var ln = lines[_li];
- if (!ln) continue;
- var colon = ln.indexOf(':');
- if (colon === -1) continue;
- var rawK = ln.substring(0, colon);
- var rawV = ln.substring(colon + 1);
- // Rhino String: 正常 key,也可能带 \x00\x80 前缀(如抓包中第一个 key = \x00\x80X-RequestId)
- // 所以 trim() key 是安全的(去掉\x00这类控制符会更好),然后 toLowerCase
- var k = rawK.replace(/[\x00-\x1f\x7f]/g, '').trim().toLowerCase();
- var v = rawV.replace(/[\x00-\x1f\x7f]/g, '').trim().toLowerCase();
- if (k === 'path') pathVal = v;
- if (k === 'content-type') contentTypeVal = v;
- }
- } catch (parseErr) {
- print.log('[Edge TTS][bin] header parse error: ' + parseErr);
- pathVal = null; contentTypeVal = null;
- }
- // 日志(不打每帧,只在首次 audio 成功和总计打,避免日志爆炸)
- if (!state._loggedFirstFrame) {
- print.log('[Edge TTS][bin] first frame: total=' + len + ' headerLen=' + headerLength + ' audioLen=' + audioLen + ' Path=' + (pathVal||'<NULL>') + ' Content-Type=' + (contentTypeVal||'<NULL>'));
- }
- // ========== 4. 按官方判断:Path == 'audio' 且 (Content-Type == 'audio/mpeg' 或 NULL) ==========
- if (pathVal !== 'audio') {
- return; // 非音频帧(例如 WebRTC 心跳等二进制帧)
- }
- if (contentTypeVal !== 'audio/mpeg') {
- // Content-Type 为 NULL 且 data 为空 = 终止帧,直接跳过
- if (contentTypeVal === null && audioLen === 0) return;
- // 有 data 但没有 Content-Type = UnexpectedResponse(官方抛异常),这里直接记日志跳过
- if (contentTypeVal === null) {
- print.log('[Edge TTS][bin] WARN: Path=audio, no Content-Type but data exists, audioLen=' + audioLen);
- return;
- }
- return;
- }
- if (audioLen <= 0) return;
- // ========== 5. 收集音频 ==========
- var chunk = new Array(audioLen);
- for (var kk2 = 0; kk2 < audioLen; kk2++) chunk[kk2] = jBytes[audioStart + kk2] & 0xff;
- state.audioChunks.push(chunk);
- state.totalLen += audioLen;
- if (!state._loggedFirstFrame) {
- state._loggedFirstFrame = true;
- var first2 = chunk[0].toString(16) + (chunk[1] < 16 ? '0' : '') + chunk[1].toString(16);
- print.log('[Edge TTS][bin] first audio bytes: 0x' + first2.toUpperCase() + ' (expect MP3: FFF3/FFF2)');
- }
- } catch (e) {
- try { print.log('[Edge TTS] onMessage uncaught error: ' + e); } catch(ee){}
- }
- },
- onClosed: function (ws, code, reason) {
- state.closed = true;
- state.connected = false;
- try { print.log('[Edge TTS][WS] onClosed 触发 code=' + code + ' reason=' + reason); } catch(e){}
- },
- onFailure: function (ws, t, response) {
- state.closed = true;
- state.connected = false;
- state.err = t ? String(t.getMessage()) : 'ws_error';
- try {
- if (response) {
- try {
- state.httpCode = response.code() || 0;
- } catch (ee) {}
- try {
- var hd = response.header('Date');
- if (hd) state.serverDate = String(hd);
- } catch (ee) {}
- }
- } catch (e) {}
- try {
- print.log('[Edge TTS][WS] ❌ onFailure: err_msg=' + (state.err || '')
- + ' HTTP_code=' + state.httpCode
- + ' Date=' + (state.serverDate || '<none>')
- + ' Throwable_type=' + (t ? String(t.getClass().getName()) : 'null'));
- } catch(e2) {}
- }
- });
- var client = new OkHttpClient.Builder()
- .pingInterval(10, TimeUnit.SECONDS)
- .connectTimeout(25, TimeUnit.SECONDS)
- .readTimeout(40, TimeUnit.SECONDS)
- .build();
- var muid = generateEdgeMuid();
- var edgeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
- + ' (KHTML, like Gecko) Chrome/' + EDGE_CHROMIUM_MAJOR_VERSION + '.0.0.0 Safari/537.36'
- + ' Edg/' + EDGE_CHROMIUM_MAJOR_VERSION + '.0.0.0';
- var request = new Request.Builder()
- .url(wsUrl)
- .addHeader('User-Agent', edgeUA)
- .addHeader('Accept-Encoding', 'gzip, deflate, br, zstd')
- .addHeader('Accept-Language', 'en-US,en;q=0.9')
- .addHeader('Pragma', 'no-cache')
- .addHeader('Cache-Control', 'no-cache')
- .addHeader('Origin', 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold')
- .addHeader('Sec-WebSocket-Version', '13')
- .addHeader('Cookie', 'muid=' + muid + ';')
- .build();
- var ws = client.newWebSocket(request, listener);
- print.log('[Edge TTS][WS] newWebSocket 已发起,等待 onOpen / onFailure / 25s timeout...');
- var w = 0;
- while (!state.connected && !state.err && w < 25000) {
- sleep(100);
- w += 100;
- }
- if (state.err) {
- try { ws.close(1000, 'err'); } catch (e) {}
- try { client.dispatcher().executorService().shutdown(); } catch (e) {}
- var code = 'error';
- if (state.httpCode === 403) code = '403';
- return { ok: false, code: code, message: state.err, serverDate: state.serverDate };
- }
- if (!state.connected) {
- try { ws.close(1000, 'timeout'); } catch (e) {}
- try { client.dispatcher().executorService().shutdown(); } catch (e) {}
- print.log('[Edge TTS][WS] ⏱️ onOpen 等待超时(25秒)未触发,已等待' + (w/1000).toFixed(1) + '秒,err=' + (state.err || '<none>'));
- return { ok: false, code: 'timeout', message: 'WebSocket 连接超时(25秒)', serverDate: state.serverDate };
- }
- w = 0;
- var lastLen = 0;
- var noGrowthMs = 0;
- while (!state.closed && !state.err && w < 45000) {
- sleep(200);
- w += 200;
- if (state.totalLen !== lastLen) {
- lastLen = state.totalLen;
- noGrowthMs = 0;
- } else {
- noGrowthMs += 200;
- if (state.totalLen > 100 && noGrowthMs > 2000) break;
- }
- }
- print.log('[Edge TTS][WS] 接收循环结束:等待=' + (w/1000).toFixed(1) + '秒 audioBytes_total=' + state.totalLen + ' closed=' + state.closed + ' err=' + (state.err||'<none>'));
- try { ws.close(1000, 'done'); } catch (e) {}
- try { client.dispatcher().executorService().shutdown(); } catch (e) {}
- if (state.err) {
- var code2 = 'error';
- if (state.httpCode === 403) code2 = '403';
- return { ok: false, code: code2, message: state.err, serverDate: state.serverDate };
- }
- if (state.totalLen < 100) {
- return { ok: false, code: 'noaudio', message: '未获取到音频数据,请检查网络或参数', serverDate: state.serverDate };
- }
- var merged = new Uint8Array(state.totalLen);
- var offset = 0;
- for (var ci = 0; ci < state.audioChunks.length; ci++) {
- var chunk = state.audioChunks[ci];
- for (var cj = 0; cj < chunk.length; cj++) merged[offset + cj] = chunk[cj];
- offset += chunk.length;
- }
- if (output) {
- if (!writeBinaryFile(output, merged)) {
- return { ok: false, code: 'write', message: '写入文件失败: ' + output, serverDate: state.serverDate };
- }
- print.log('[Edge TTS] 已保存: ' + output + ' (' + state.totalLen + ' 字节)');
- return { ok: true, output: output, serverDate: state.serverDate };
- }
- return { ok: true, output: merged, serverDate: state.serverDate };
- }
- /**
- * Edge TTS 核心生成(同步阻塞,关键字接口)—— 带官方 DRM 时钟偏移自动校准+重试
- *
- * @param {Object} options
- * @param {string} options.text - 要合成的文字(必填)
- * @param {string} [options.voice] - 音色ID,默认 zh-CN-XiaoxiaoNeural
- * @param {string} [options.rate] - 语速 "+0%" / "-20%" / "+50%"
- * @param {string} [options.volume] - 音量 "+0%" / "+50%"
- * @param {string} [options.pitch] - 音调 "+0Hz" / "-10Hz"
- * @param {string} [options.output] - 输出路径(.mp3),不填返回 Uint8Array
- * @returns {string|Uint8Array|null} 成功返回路径或 Uint8Array,失败返回 null
- */
- function generateEdge(options) {
- var opts = options || {};
- if (!opts.text || !String(opts.text).trim()) {
- print.log('[Edge TTS] 缺少必填参数: text');
- return null;
- }
- var sessionOpts = {
- text: opts.text,
- voice: opts.voice || DEFAULT_EDGE_VOICE,
- rate: opts.rate || '+0%',
- volume: opts.volume || '+0%',
- pitch: opts.pitch || '+0Hz',
- output: opts.output || null
- };
- // 第 1 次尝试
- var r = tryEdgeOnce(sessionOpts);
- if (r.ok) return r.output;
- // 官方 DRM.handle_client_response_error:
- // 若失败且带 Date 头 → 计算 skew = serverDate - localNow → 修正 skew → 自动重试
- if (r.code === '403' && r.serverDate) {
- print.log('[Edge TTS][403] 收到服务器 Date 头: ' + String(r.serverDate));
- var serverTs = parseRfc2616Date(r.serverDate);
- var localTsNow = java.lang.System.currentTimeMillis() / 1000.0;
- if (serverTs !== null) {
- var skew = serverTs - localTsNow;
- print.log('[Edge TTS][403] serverTs_parsed=' + serverTs.toFixed(3)
- + ' localNow=' + localTsNow.toFixed(3)
- + ' skew=' + skew.toFixed(3));
- var oldSkew = EDGE_CLOCK_SKEW_SECONDS;
- EDGE_CLOCK_SKEW_SECONDS += skew;
- print.log('[Edge TTS] 校准时钟偏差: old_skew=' + oldSkew.toFixed(3)
- + ' +delta=' + skew.toFixed(3)
- + ' → new_skew=' + EDGE_CLOCK_SKEW_SECONDS.toFixed(3) + ' 秒(403 后自动重试)');
- var r2 = tryEdgeOnce(sessionOpts);
- if (r2.ok) return r2.output;
- print.log('[Edge TTS] 重试后仍失败: code=' + r2.code + ' msg=' + r2.message
- + (r2.serverDate ? ' serverDate=' + r2.serverDate : ''));
- return null;
- } else {
- print.log('[Edge TTS][403] 日期解析失败!无法计算时钟偏差,放弃重试');
- }
- }
- // 无 Date 头或非 403 错误,直接打印
- print.log('[Edge TTS] 失败(' + r.code + '): ' + r.message);
- return null;
- }
- // ==================== 豆包 VoiceGenie TTS 核心 ====================
- var DOUBAO_AUDIO_CONFIG = { bit_rate: 32000, format: 'ogg_opus', sample_rate: 24000 };
- /**
- * 豆包 VoiceGenie TTS 核心生成(同步阻塞,关键字接口)
- */
- function generateDoubao(options) {
- var opts = options || {};
- var cookies = opts.cookies;
- var conversationId = opts.conversationId;
- var messageId = opts.messageId;
- var questionId = opts.questionId || null;
- var sectionId = opts.sectionId || '0';
- var speaker = opts.speaker || DEFAULT_DOUBAO_VOICE;
- var speechRate = opts.speechRate || 1.0;
- var pitch = opts.pitch || 0;
- var audioConfig = opts.audioConfig || DOUBAO_AUDIO_CONFIG;
- var output = opts.output || null;
- if (!cookies) { print.log('[豆包 TTS] 缺少必填参数: cookies'); return null; }
- if (!conversationId) { print.log('[豆包 TTS] 缺少必填参数: conversationId'); return null; }
- if (!messageId) { print.log('[豆包 TTS] 缺少必填参数: messageId'); return null; }
- var cookieStr = cookieObjToString(cookies);
- var qId = questionId || messageId;
- var sessionOpts = {
- conversationId: conversationId, questionId: qId, messageId: messageId,
- sectionId: sectionId, speaker: speaker, speechRate: speechRate,
- pitch: pitch, audioConfig: audioConfig, cookieStr: cookieStr
- };
- var state = {
- connected: false,
- closed: false,
- sessionId: null,
- gotTTSEnded: false,
- audioChunks: [],
- totalLen: 0,
- err: null
- };
- var wsUrl = buildDoubaoWsUrl(cookieStr);
- var listener = new WebSocketListener({
- onOpen: function (ws, response) {
- state.connected = true;
- try {
- ws.send(ByteString.of(toJavaBytes(buildRequestFrame('StartTask', '{}', null))));
- ws.send(ByteString.of(toJavaBytes(buildRequestFrame('Ping', '{}', null))));
- } catch (e) {
- state.err = '发送 StartTask 失败: ' + e;
- }
- },
- onMessage: function (ws, message) {
- try {
- if (typeof message === 'string') return;
- if (!message || !message.toByteArray) return;
- var jBytes = message.toByteArray();
- var u8 = new Uint8Array(jBytes.length);
- for (var i = 0; i < jBytes.length; i++) u8[i] = jBytes[i] & 0xff;
- var parsed = parseResponseFrame(u8);
- var ev = parsed.eventType;
- if (ev === 'TaskStarted') {
- var payload1 = buildDoubaoSessionPayload(sessionOpts);
- ws.send(ByteString.of(toJavaBytes(buildRequestFrame('StartSession', payload1, null))));
- } else if (ev === 'SessionStarted') {
- state.sessionId = uuid();
- ws.send(ByteString.of(toJavaBytes(buildRequestFrame('Ping', '{}', state.sessionId))));
- var payload2 = buildDoubaoSessionPayload(sessionOpts);
- ws.send(ByteString.of(toJavaBytes(buildRequestFrame('StartTTS', payload2, state.sessionId))));
- } else if (ev === 'TTSResponse') {
- if (parsed.audioData && parsed.audioData.length > 0) {
- state.audioChunks.push({ seq: parsed.seq, data: parsed.audioData });
- state.totalLen += parsed.audioData.length;
- }
- } else if (ev === 'TTSEnded') {
- state.gotTTSEnded = true;
- try { ws.close(1000, 'done'); } catch (e) {}
- }
- } catch (e) {}
- },
- onClosed: function (ws, code, reason) {
- state.closed = true;
- state.connected = false;
- },
- onFailure: function (ws, t, response) {
- state.closed = true;
- state.connected = false;
- state.err = t ? String(t.getMessage()) : 'ws_error';
- }
- });
- var client = new OkHttpClient.Builder()
- .pingInterval(10, TimeUnit.SECONDS)
- .connectTimeout(15, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.SECONDS)
- .build();
- var request = new Request.Builder()
- .url(wsUrl)
- .addHeader('Cookie', cookieStr)
- .addHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36')
- .addHeader('Origin', 'https://www.doubao.com')
- .build();
- var ws = client.newWebSocket(request, listener);
- var w = 0;
- while (!state.connected && !state.err && w < 15000) {
- sleep(100);
- w += 100;
- }
- if (state.err) {
- print.log('[豆包 TTS] 连接失败: ' + state.err);
- try { ws.close(1000, 'err'); } catch (e) {}
- try { client.dispatcher().executorService().shutdown(); } catch (e) {}
- return null;
- }
- if (!state.connected) {
- print.log('[豆包 TTS] WebSocket 连接超时 (15秒)');
- try { ws.close(1000, 'timeout'); } catch (e) {}
- try { client.dispatcher().executorService().shutdown(); } catch (e) {}
- return null;
- }
- w = 0;
- var lastLen = 0;
- var noGrowthMs = 0;
- while (!state.closed && !state.gotTTSEnded && !state.err && w < 60000) {
- sleep(200);
- w += 200;
- if (state.totalLen !== lastLen) {
- lastLen = state.totalLen;
- noGrowthMs = 0;
- } else {
- noGrowthMs += 200;
- if (state.totalLen > 0 && noGrowthMs > 3000) break;
- }
- }
- try { ws.close(1000, 'done'); } catch (e) {}
- try { client.dispatcher().executorService().shutdown(); } catch (e) {}
- if (state.err) {
- print.log('[豆包 TTS] ' + state.err);
- return null;
- }
- if (state.audioChunks.length === 0) {
- print.log('[豆包 TTS] 未收到任何音频数据,请检查 cookies/对话参数');
- return null;
- }
- state.audioChunks.sort(function (a, b) { return a.seq - b.seq; });
- var merged = new Uint8Array(state.totalLen);
- var offset = 0;
- for (var ci = 0; ci < state.audioChunks.length; ci++) {
- var data = state.audioChunks[ci].data;
- for (var cj = 0; cj < data.length; cj++) {
- merged[offset + cj] = data[cj];
- }
- offset += data.length;
- }
- if (output) {
- if (writeBinaryFile(output, merged)) {
- print.log('[豆包 TTS] 已保存: ' + output + ' (' + state.totalLen + ' 字节)');
- return output;
- }
- print.log('[豆包 TTS] 写入文件失败: ' + output);
- return null;
- }
- return merged;
- }
- // ==================== 直接运行示例 ====================
- (function runDemo() {
- print.log('=== AIWROK TTS CORE 单文件版 ===');
- print.log('时间: ' + new Date().toString());
- print.log('');
- // --- Edge TTS 示例 ---
- print.log('[1] Edge TTS 生成中...');
- var demoText = '庚子年后京城的买卖不好做,前门大街倒了三家茶馆,唯独德顺的王掌柜,日子反倒越过越舒展。'
- + '熟客们都打趣,说老王是祖坟冒了青烟,平白无故的好运往怀里撞。'
- + '王掌柜有个旁人瞧不上的小习惯:每天天刚蒙蒙亮,伙计还没上门,他先搬条凳坐柜台后头,'
- + '就着油灯的光,在麻纸本子上写半页字。写的不是什么圣贤文章,全是鸡零狗碎的闲事:'
- + '张二爷爱喝焖得酽的香片,李三赊了俩铜子儿的茶钱,驴打滚剩了八块卖不动,街对过杂货铺的东家爱喝茉莉双熏。'
- + '常来蹭茶喝的混混儿刘四笑他:“王掌柜,你这是要考秀才啊?开茶馆卖水得了,费那洋劲干嘛?”'
- + '王掌柜也不恼,嘿嘿一笑,把本子收进抽屉,第二天照旧写。'
- + '那年春上茶叶价涨得邪乎,别家茶馆要么涨价把客人挤走,要么换碎茶末子砸招牌。'
- + '唯独德顺,价没大涨,茶味还跟从前一样地道。满街的人都说老王运气好,提前囤着好茶了。'
- + '哪是运气?他那半页纸上,早记着“每年开春茶价必涨三成,头年秋里定货最稳”。'
- + '这不是他天生会算,是天天写、年年记,摸出来的老规矩。旁人年年临渴掘井,他早把井挖在了前头。'
- + '还有一回,有个南下的绸缎商来城里收贷,进茶馆歇脚。王掌柜一眼就想起,'
- + '前阵子本子上记过这位客官爱喝碧螺春,还托人打听过京里靠谱的绸缎庄。'
- + '他一边上茶一边顺嘴提了两家字号,连东家的脾气秉性都顺带说了。'
- + '那商人感激不尽,往后带了好几拨同行来,成了固定的大主顾。'
- + '旁人又说,老王会来事,平白撞上了贵人。其实哪是撞的?是他天天记着客人的喜好,机会递到跟前,伸手就接住了。'
- + '日子久了,德顺的生意越做越稳,别家倒闭的伙计都来投奔。'
- + '有人专门来讨教“转运”的法子,王掌柜就把那本麻纸本子翻出来给人看:'
- + '“哪有什么好运?我就是每天早起写半页纸,写着写着,账也清了,人也熟了,该避的坑避了,该抓的机会也没跑。”'
- + '私塾的陈先生呷了口茶,慢悠悠补了句:“这就叫拱心石的习惯。看着是每天半页纸的小事,'
- + '实则把记性、眼力、心思都串起来了。习惯成了本能,就不用天天费脑子琢磨运气。'
- + '提示是开门点灯,动作是记账识人,甜头是买卖顺当,日子一长,好运就成了自然而然的事儿。”'
- + '也有人不服气,买了本子回去学,没半个月就嫌麻烦,扔在了墙角。'
- + '照旧是稀里糊涂过日子,赔了赚了都不知道根由,遇着不顺心就怨命不好。'
- + '王掌柜后来常跟伙计说,天底下的好运,从来不是天上掉下来的。'
- + '就像这茶馆的门轴,天天擦着润着,开门就顺当;锈住了,再使劲也推不开。'
- + '每天半页纸,就是那点润轴的油,看着不起眼,日子久了,就能撑起整份日子的安稳。';
- var edgeOut = generateEdge({
- text: demoText,
- voice: DEFAULT_EDGE_VOICE,
- rate: '+0%',
- volume: '+0%',
- pitch: '+0Hz',
- output: '/sdcard/edge_tts_demo.mp3'
- });
- if (edgeOut) {
- print.log(' 生成成功: ' + edgeOut);
- } else {
- print.log(' 生成失败');
- }
- print.log('');
- // --- 豆包 TTS 示例(需真实参数,否则会失败) ---
- print.log('[2] 豆包 TTS 示例(需填入真实参数才能运行)...');
- var doubaoOut = generateDoubao({
- cookies: 'sessionid=YOUR_SESSIONID; session_ss=YOUR_SESSION_SS',
- conversationId: 'YOUR_CONVERSATION_ID',
- messageId: 'YOUR_MESSAGE_ID',
- speaker: DEFAULT_DOUBAO_VOICE,
- speechRate: 1.0,
- output: '/sdcard/doubao_tts_demo.ogg'
- });
- if (doubaoOut) {
- print.log(' 生成成功: ' + doubaoOut);
- } else {
- print.log(' 预期失败(参数未填)');
- }
- // --- 打印音色列表 ---
- print.log('');
- print.log('=== 可用音色 ===');
- print.log('Edge TTS (' + EDGE_VOICES.length + ' 个):');
- for (var i = 0; i < EDGE_VOICES.length; i++) {
- print.log(' ' + EDGE_VOICES[i].id + ' [' + EDGE_VOICES[i].name + ']');
- }
- print.log('');
- print.log('豆包 TTS (' + DOUBAO_VOICES.length + ' 个):');
- for (var j = 0; j < DOUBAO_VOICES.length; j++) {
- print.log(' ' + DOUBAO_VOICES[j].id + ' [' + DOUBAO_VOICES[j].name + ']');
- }
- })();
- exit();
复制代码
| |  | |  |
|