注册 登录
发贴工具
查看: 4|回复: 0
打印 上一主题 下一主题

[24小时收录超级好的网站] 语音合成核心模块AIWROK 单文件版

[复制链接]

2601

主题

2649

帖子

1万

积分

积分
15782
跳转到指定楼层
楼主
语音合成核心模块AIWROK 单文件版
语音合成核心模块AIWROK 单文件版 群发软件发帖工具
  1. /**
  2. * ============================================================================
  3. *  AIWROK TTS CORE - 语音合成核心模块(AIWROK 单文件版)
  4. * ============================================================================
  5. */

  6. // ==================== 导入 Java 类(AIWROK 内置) ====================

  7. importClass(Packages.java.lang.Thread);
  8. importClass(Packages.java.io.File);
  9. importClass(Packages.java.io.FileOutputStream);
  10. importClass(Packages.java.io.BufferedOutputStream);
  11. importClass(Packages.java.security.MessageDigest);
  12. // 注意:Array 和 Byte 不做 importClass,会和 JS 全局对象冲突
  13. // 代码里直接用完整类名:java.lang.reflect.Array / java.lang.Byte
  14. importClass(Packages.okhttp3.OkHttpClient);
  15. importClass(Packages.okhttp3.Request);
  16. importClass(Packages.okhttp3.RequestBody);
  17. importClass(Packages.okhttp3.WebSocketListener);
  18. importClass(Packages.okio.ByteString);
  19. importClass(Packages.okio.Buffer);
  20. importClass(Packages.java.util.concurrent.TimeUnit);

  21. // ==================== 音色列表 ====================

  22. var EDGE_VOICES = [
  23.     { id: "zh-CN-XiaoxiaoNeural",   name: "晓晓 (甜美女声)" },
  24.     { id: "zh-CN-XiaoyiNeural",     name: "晓伊 (软萌少女)" },
  25.     { id: "zh-CN-XiaohanNeural",    name: "晓涵 (知性御姐)" },
  26.     { id: "zh-CN-XiaomengNeural",   name: "晓梦 (温柔治愈)" },
  27.     { id: "zh-CN-XiaomoNeural",     name: "晓墨 (成熟稳重)" },
  28.     { id: "zh-CN-XiaoxuanNeural",   name: "晓萱 (活泼可爱)" },
  29.     { id: "zh-CN-XiaoyanNeural",    name: "晓颜 (新闻女主播)" },
  30.     { id: "zh-CN-XiaochenNeural",   name: "晓辰 (温暖女声)" },
  31.     { id: "zh-CN-XiaoshuangNeural", name: "晓双 (儿童女声)" },
  32.     { id: "zh-CN-XiaoruiNeural",    name: "晓睿 (老年女声)" },
  33.     { id: "zh-CN-YunxiNeural",      name: "云希 (年轻男声)" },
  34.     { id: "zh-CN-YunjianNeural",    name: "云健 (成熟男声)" },
  35.     { id: "zh-CN-YunyangNeural",    name: "云扬 (新闻播音)" },
  36.     { id: "zh-CN-YunxiaNeural",     name: "云夏 (少年男声)" },
  37.     { id: "zh-CN-YunyeNeural",      name: "云野 (故事讲述)" },
  38.     { id: "zh-CN-YunfengNeural",    name: "云枫 (老年男声)" },
  39.     { id: "zh-TW-HsiaoChenNeural",  name: "曉臻 (台湾女声)" },
  40.     { id: "zh-TW-YunJheNeural",     name: "雲哲 (台湾男声)" },
  41.     { id: "zh-HK-HiuMaanNeural",    name: "曉曼 (粤语女声)" },
  42.     { id: "zh-HK-WanLungNeural",    name: "雲龍 (粤语男声)" }
  43. ];

  44. var DOUBAO_VOICES = [
  45.     { id: "zh_female_taozi_conversation_v4_wvae_bigtts",  name: "桃子 (温柔女声)" },
  46.     { id: "zh_male_wennuanahu_radar_v4_wvae_bigtts",      name: "温暖 (男声)" },
  47.     { id: "zh_female_qingxinahu_radar_v4_wvae_bigtts",    name: "清新 (女声)" },
  48.     { id: "zh_male_zhiyu_radar_v5_wvae_bigtts",           name: "治愈 (男声)" },
  49.     { id: "zh_female_wanwanxiaohe_moon_bigtts",           name: "弯弯小贺 (女童)" },
  50.     { id: "zh_female_linjiaxiaoyu_moon_bigtts",           name: "邻家小语 (少女)" }
  51. ];

  52. var DEFAULT_EDGE_VOICE = "zh-CN-XiaoxiaoNeural";
  53. var DEFAULT_DOUBAO_VOICE = "zh_female_taozi_conversation_v4_wvae_bigtts";

  54. // ==================== 通用工具函数 ====================

  55. /**
  56. * 生成 UUID v4(Rhino ES5 兼容)
  57. */
  58. function uuid() {
  59.     var chars = '0123456789abcdef';
  60.     var s = [];
  61.     for (var i = 0; i < 36; i++) s[i] = chars[Math.floor(Math.random() * 16)];
  62.     s[14] = '4';
  63.     s[19] = chars[(parseInt(s[19], 16) & 0x3) | 0x8];
  64.     s[8] = s[13] = s[18] = s[23] = '-';
  65.     return s.join('');
  66. }

  67. /** XML 转义 */
  68. function escapeXml(str) {
  69.     return String(str)
  70.         .replace(/&/g, '&')
  71.         .replace(/</g, '<')
  72.         .replace(/>/g, '>')
  73.         .replace(/"/g, '"')
  74.         .replace(/'/g, ''');
  75. }

  76. /**
  77. * 把 JS 的 ArrayBuffer / Uint8Array / number[] 转成 Java byte[]
  78. * Rhino 里 ByteString.of() 和 Java IO 都需要原生 byte[]
  79. */
  80. function toJavaBytes(data) {
  81.     var view;
  82.     if (data instanceof ArrayBuffer) {
  83.         view = new Uint8Array(data);
  84.     } else if (data instanceof Uint8Array) {
  85.         view = data;
  86.     } else if (data && typeof data.length === 'number') {
  87.         view = new Uint8Array(data.length);
  88.         for (var i2 = 0; i2 < data.length; i2++) view[i2] = data[i2];
  89.     } else {
  90.         return java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 0);
  91.     }
  92.     var jBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, view.length);
  93.     for (var k = 0; k < view.length; k++) {
  94.         var v = view[k] & 0xff;
  95.         jBytes[k] = (v > 127) ? (v - 256) : v;
  96.     }
  97.     return jBytes;
  98. }

  99. /**
  100. * 把 Uint8Array / number[] / ArrayBuffer 写入文件
  101. * @param {string} path
  102. * @param {Uint8Array|number[]|ArrayBuffer} data
  103. * @return {boolean}
  104. */
  105. function writeBinaryFile(path, data) {
  106.     try {
  107.         var jBytes = toJavaBytes(data);
  108.         if (!jBytes || jBytes.length === 0) return false;

  109.         // 确保父目录存在
  110.         var f = new java.io.File(path);
  111.         var parent = f.getParentFile();
  112.         if (parent && !parent.exists()) parent.mkdirs();

  113.         var fos = new java.io.FileOutputStream(f);
  114.         var out = new java.io.BufferedOutputStream(fos);
  115.         out.write(jBytes);
  116.         out.flush();
  117.         out.close();
  118.         fos.close();
  119.         return true;
  120.     } catch (e) {
  121.         print.log("[writeBinaryFile] 失败: " + e);
  122.         return false;
  123.     }
  124. }

  125. /**
  126. * 简易 sleep(不抛异常)
  127. */
  128. function sleep(ms) {
  129.     try { java.lang.Thread.sleep(ms); } catch (e) {}
  130. }

  131. /**
  132. * 字符串左侧补齐(Rhino 不支持 padStart)
  133. */
  134. function padStart(s, len, ch) {
  135.     s = String(s);
  136.     while (s.length < len) s = ch + s;
  137.     return s;
  138. }

  139. // ==================== Protobuf 编解码(豆包用) ====================

  140. function encodeVarint(v) {
  141.     var out = [];
  142.     v = v >>> 0;
  143.     while (v > 0x7f) {
  144.         out.push((v & 0x7f) | 0x80);
  145.         v = v >>> 7;
  146.     }
  147.     out.push(v & 0x7f);
  148.     return out;
  149. }

  150. function encodeBytesField(fieldNum, data) {
  151.     var bytes;
  152.     if (typeof data === 'string') {
  153.         // UTF-8 编码(不能用 unescape,中文会乱码)
  154.         var u8 = unescape(encodeURIComponent(data));
  155.         bytes = [];
  156.         for (var i = 0; i < u8.length; i++) bytes.push(u8.charCodeAt(i) & 0xff);
  157.     } else if (data && data.length !== undefined) {
  158.         bytes = [];
  159.         for (var j = 0; j < data.length; j++) bytes.push(data[j] & 0xff);
  160.     } else {
  161.         bytes = [];
  162.     }
  163.     var tag = (fieldNum << 3) | 2;
  164.     return encodeVarint(tag).concat(encodeVarint(bytes.length), bytes);
  165. }

  166. /**
  167. * 构造完整的 VoiceGenie 请求帧,返回 ArrayBuffer
  168. */
  169. function buildRequestFrame(event, payload, sessionId) {
  170.     var frame = [];
  171.     frame = frame.concat(encodeBytesField(2, 'GOqQpfo1fO7slHv8'));
  172.     frame = frame.concat(encodeBytesField(3, 'VoiceGenie'));
  173.     frame = frame.concat(encodeBytesField(5, event));
  174.     frame = frame.concat(encodeBytesField(6, payload));
  175.     if (sessionId) {
  176.         frame = frame.concat(encodeBytesField(8, sessionId));
  177.     }
  178.     // number[] → ArrayBuffer
  179.     var ab = new ArrayBuffer(frame.length);
  180.     var v = new Uint8Array(ab);
  181.     for (var i = 0; i < frame.length; i++) v[i] = frame[i];
  182.     return ab;
  183. }

  184. /**
  185. * 解析服务器返回的 protobuf 帧
  186. */
  187. function parseResponseFrame(buf) {
  188.     var a = (buf instanceof Uint8Array) ? buf : new Uint8Array(buf);
  189.     var fields = [];
  190.     var i = 0;

  191.     function readVarint() {
  192.         var v = 0, s = 0;
  193.         while (i < a.length) {
  194.             var b = a[i++];
  195.             v |= (b & 0x7f) << s;
  196.             if (!(b & 0x80)) return v >>> 0;
  197.             s += 7;
  198.             if (s > 35) break;
  199.         }
  200.         return v >>> 0;
  201.     }

  202.     while (i < a.length) {
  203.         var tag;
  204.         try { tag = readVarint(); } catch (e) { break; }
  205.         if (tag === 0) break;
  206.         var fieldNum = tag >> 3;
  207.         var wireType = tag & 7;
  208.         var field = { fieldNum: fieldNum, wireType: wireType };

  209.         if (wireType === 0) {
  210.             field.value = readVarint();
  211.         } else if (wireType === 2) {
  212.             var len = readVarint();
  213.             // 兼容 Rhino Uint8Array.subarray/slice
  214.             var sliceArr = [];
  215.             for (var kk = 0; kk < len; kk++) sliceArr.push(a[i + kk]);
  216.             field.data = sliceArr; // number[]
  217.             i += len;
  218.             if (len <= 8000) {
  219.                 try {
  220.                     // 转 Java byte[] 再 UTF-8 解码(中文安全)
  221.                     var jb = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, len);
  222.                     for (var m = 0; m < len; m++) {
  223.                         var bv = a[i - len + m] & 0xff;
  224.                         jb[m] = (bv > 127) ? (bv - 256) : bv;
  225.                     }
  226.                     field.asString = String(new java.lang.String(jb, "UTF-8"));
  227.                 } catch (e) {}
  228.             }
  229.         } else if (wireType === 5) {
  230.             i += 4;
  231.         } else if (wireType === 1) {
  232.             i += 8;
  233.         } else {
  234.             break;
  235.         }
  236.         fields.push(field);
  237.     }

  238.     var result = { eventType: null, payload: null, audioData: null, seq: -1, sessionId: null };
  239.     for (var f = 0; f < fields.length; f++) {
  240.         var fld = fields[f];
  241.         if (fld.fieldNum === 1) result.sessionId = fld.asString || null;
  242.         if (fld.fieldNum === 4) result.eventType = fld.asString || null;
  243.         if (fld.fieldNum === 7) result.payload = fld.data || null;
  244.         if (fld.fieldNum === 8) result.audioData = fld.data; // number[]
  245.         if (fld.fieldNum === 9) result.seq = fld.value;
  246.     }
  247.     return result;
  248. }

  249. // ==================== 豆包 Cookie / UID 工具 ====================

  250. function cookieObjToString(cookies) {
  251.     if (typeof cookies === 'string') return cookies;
  252.     if (cookies && typeof cookies === 'object') {
  253.         // ES5 没有 Object.entries
  254.         var parts = [];
  255.         for (var k in cookies) {
  256.             if (cookies.hasOwnProperty(k)) {
  257.                 parts.push(k + '=' + cookies[k]);
  258.             }
  259.         }
  260.         return parts.join('; ');
  261.     }
  262.     return '';
  263. }

  264. function getCookieFromStr(cookieStr, name) {
  265.     if (!cookieStr) return null;
  266.     var m = cookieStr.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]+)'));
  267.     return m ? m[1] : null;
  268. }

  269. function getUid(cookieStr) {
  270.     var uidCookie = getCookieFromStr(cookieStr, 'uid_tt') || getCookieFromStr(cookieStr, 'uid');
  271.     if (uidCookie) {
  272.         try {
  273.             // 用 Java BASE64 解码(Rhino 没有 atob)
  274.             var decoded = String(java.util.Base64.getDecoder().decode(uidCookie));
  275.             if (/^\d+$/.test(decoded)) return decoded;
  276.         } catch (e) {}
  277.         if (/^\d+$/.test(uidCookie)) return uidCookie;
  278.     }
  279.     return String(Date.now() % 1000000000000);
  280. }

  281. function buildDoubaoWsUrl(cookieStr) {
  282.     var deviceId = getCookieFromStr(cookieStr, 'device_id') ||
  283.                    getCookieFromStr(cookieStr, 's_v_web_id') ||
  284.                    (String(Date.now()) + padStart(String(Math.floor(Math.random() * 1e6)), 6, '0'));
  285.     var webId = getCookieFromStr(cookieStr, 'web_id') ||
  286.                 getCookieFromStr(cookieStr, 's_v_web_id') ||
  287.                 deviceId;

  288.     var params = [
  289.         'api_app_key=GOqQpfo1fO7slHv8',
  290.         'namespace=VoiceGenie',
  291.         'version_code=20800',
  292.         'language=zh',
  293.         'device_platform=web',
  294.         'pkg_type=release_version',
  295.         'pc_version=3.30.5',
  296.         'region=CN',
  297.         'sys_region=CN',
  298.         'samantha_web=1',
  299.         'use-olympus-account=1',
  300.         'doubao_device_platform=web',
  301.         'aid=497858',
  302.         'real_aid=497858',
  303.         'device_id=' + deviceId,
  304.         'doubao_pc_version=3.30.5',
  305.         'web_id=' + webId,
  306.         'tea_uuid=' + webId,
  307.         'web_platform=browser',
  308.         'web_tab_id=' + uuid()
  309.     ];
  310.     return 'wss://frontier-audio-web-ws.doubao.com/api/v2/sami/voicegenie?' + params.join('&');
  311. }

  312. function buildDoubaoSessionPayload(opts) {
  313.     var replyUniqueKey = uuid();
  314.     var queryLocalMessageId = uuid();
  315.     var uid = getUid(opts.cookieStr);

  316.     var payload = {
  317.         business: 1,
  318.         conversation_id: opts.conversationId,
  319.         chat: {
  320.             bot_id: '7338286299411103781',
  321.             conversation_id: opts.conversationId,
  322.             question_id: opts.questionId,
  323.             message_id: opts.messageId,
  324.             uid: uid,
  325.             new_conversation: false,
  326.             chat_next: '1',
  327.             enable_toast_reading: 'true',
  328.             extra: { chat_next: '1', reply_unique_key: replyUniqueKey, query_local_message_id: queryLocalMessageId },
  329.             reply_unique_key: replyUniqueKey,
  330.             section_id: opts.sectionId,
  331.             local_message_id: uuid(),
  332.             query_local_message_id: queryLocalMessageId
  333.         },
  334.         request_type: 4,
  335.         enable_text_reading: true,
  336.         interrupt_type: 0,
  337.         query_mode: 2,
  338.         tts: {
  339.             speaker: opts.speaker,
  340.             audio_config: opts.audioConfig,
  341.             extra: {
  342.                 chat_next: '1',
  343.                 query_local_message_id: queryLocalMessageId,
  344.                 post_process: { pitch: opts.pitch || 0, speech_rate: opts.speechRate || 1.0 }
  345.             }
  346.         },
  347.         extra: {
  348.             enable_toast_reading: 'true',
  349.             chat_next: '1',
  350.             uid: uid,
  351.             section_id: opts.sectionId,
  352.             reply_unique_key: replyUniqueKey,
  353.             query_local_message_id: queryLocalMessageId,
  354.             enable_text_simple_report: true,
  355.             enable_section_report: true,
  356.             enable_card_simple_report: true,
  357.             enable_image_asr_ctx: true,
  358.             disable_markdown_filter: true,
  359.             enable_asr_audio_context: false,
  360.             is_server_error_retry: false
  361.         }
  362.     };
  363.     return JSON.stringify(payload);
  364. }

  365. // ==================== Edge TTS 核心(1:1 对应 Python rany2/edge-tts 官方算法) ====================

  366. var EDGE_TTS_WS_URL = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
  367. var EDGE_TTS_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';

  368. // 完全对齐官方 constants.py CHROMIUM_FULL_VERSION
  369. var EDGE_CHROMIUM_FULL_VERSION = '143.0.3650.75';
  370. var EDGE_CHROMIUM_MAJOR_VERSION  = '143';
  371. var EDGE_TTS_GEC_VERSION = '1-' + EDGE_CHROMIUM_FULL_VERSION;
  372. var EDGE_WIN_EPOCH_SECONDS = 11644473600;

  373. /**
  374. * 全局 DRM 时钟偏差(单位秒,正数=服务器比本地快)
  375. * 官方:DRM.clock_skew_seconds,403失败时从服务器 Date 头校准后自动重试
  376. */
  377. var EDGE_CLOCK_SKEW_SECONDS = 0.0;

  378. /**
  379. * 解析 RFC 2616 GMT 日期(RFC_1123_DATE_TIME 格式)
  380. * 优先使用 Java DateTimeFormatter,避免自己正则解析出错。
  381. *
  382. * @param {string|null|undefined} dateStr 例如 "Sat, 08 Aug 2026 07:20:58 GMT"
  383. * @returns {number|null} UTC 秒数(float),失败返回 null
  384. */
  385. function parseRfc2616Date(dateStr) {
  386.     if (!dateStr) return null;
  387.     var s = String(dateStr).trim();
  388.     try {
  389.         // === 方案1:Java 8+ java.time(安卓 7.0+ 支持,AIWROK 运行环境一般都有) ===
  390.         var formatter = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
  391.         var temporal = formatter.parse(s);
  392.         var zdt = java.time.ZonedDateTime.from(temporal);
  393.         var epochSec = zdt.toEpochSecond();  // long
  394.         // 注意:RFC 2616 日期不提供毫秒,只到秒
  395.         return parseFloat(String(epochSec));
  396.     } catch (e1) {
  397.         try {
  398.             // === 方案2:老安卓 SimpleDateFormat ===
  399.             var sdf = new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", java.util.Locale.US);
  400.             sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
  401.             var d = sdf.parse(s);
  402.             return parseFloat(String(d.getTime())) / 1000.0;
  403.         } catch (e2) {
  404.             // === 方案3:最后兜底 Rhino JS 原生 Date.parse ===
  405.             try {
  406.                 var fb = Date.parse(s);
  407.                 if (!isNaN(fb)) return fb / 1000.0;
  408.             } catch (e3) {}
  409.             return null;
  410.         }
  411.     }
  412. }

  413. /**
  414. * 生成 MUID(32位随机大写HEX)
  415. * 官方 DRM.generate_muid → headers_with_muid 塞到 Cookie 里
  416. */
  417. function generateEdgeMuid() {
  418.     var hex = '0123456789ABCDEF';
  419.     var sb = '';
  420.     for (var i = 0; i < 32; i++) {
  421.         sb += hex.charAt(Math.floor(Math.random() * 16));
  422.     }
  423.     return sb;
  424. }

  425. /**
  426. * 生成 Sec-MS-GEC 签名(严格对齐官方 drm.py DRM.generate_sec_ms_gec + clock_skew)
  427. *
  428. * 官方 Python 流程:
  429. *   ticks = dt.now(tz.utc).timestamp() + DRM.clock_skew_seconds
  430. *   ticks += WIN_EPOCH
  431. *   ticks -= ticks % 300
  432. *   ticks *= 1e7
  433. *   str = f"{ticks:.0f}{TOKEN}"
  434. *   return sha256(str.encode("ascii")).hexdigest().upper()
  435. */
  436. var _EDGE_GEC_LOG_ONCE = false;  // 只打印一次避免刷屏
  437. function generateSecMsGec() {
  438.     try {
  439.         var unixSecFloat = (java.lang.System.currentTimeMillis() / 1000.0) + EDGE_CLOCK_SKEW_SECONDS;
  440.         var ticks = unixSecFloat + EDGE_WIN_EPOCH_SECONDS;
  441.         // 对负数取模对齐 Python:Python % 永远非负,Rhino/JS 可能返回负数 → 手动归一化
  442.         var mod = ticks % 300;
  443.         if (mod < 0) mod += 300;
  444.         ticks = ticks - mod;
  445.         ticks = ticks * 10000000.0;
  446.         var ticksLong = java.lang.Math.round(ticks);
  447.         var ticksStr = String(ticksLong);
  448.         var src = ticksStr + EDGE_TTS_TOKEN;
  449.         var md = MessageDigest.getInstance('SHA-256');
  450.         var srcBytes = new java.lang.String(src).getBytes('US-ASCII');
  451.         var hash = md.digest(srcBytes);
  452.         var sb = new java.lang.StringBuilder(hash.length * 2);
  453.         for (var i = 0; i < hash.length; i++) {
  454.             var h = hash[i] & 0xff;
  455.             // ============================================================
  456.             // 修复 Rhino 平台大坑:
  457.             // java.lang.Integer.toHexString() 返回的是 java.lang.String(不是 JS String!)
  458.             // → java.lang.String 的 .length 是 METHOD/FUNCTION,不是 JS 数值属性 length
  459.             // → 旧代码:if (s.length < 2) → NaN < 2 → 永远 FALSE!
  460.             // → 结果:只要 byte < 0x10(高位为 0),补零就被跳过!
  461.             // → 每少一个零就错位 4 bit,导致整个 GEC 和官方差好几个字符 → 微软 403
  462.             // ============================================================
  463.             var hexJava = java.lang.Integer.toHexString(h);
  464.             // ① 强转为 JS 原生 String(此时 .length 就是数值属性)
  465.             var hex = String(hexJava);
  466.             // ② 也可以用 Java 方法 .length() 双重保险(任一方式都可以)
  467.             if (hex.length < 2) sb.append('0');
  468.             sb.append(hex);
  469.         }
  470.         var gec = String(sb.toString()).toUpperCase();

  471.         // ===== DEBUG:每次生成 GEC 时,只在第一次 / 403 前后打印关键参数 =====
  472.         // 用户可复制下面这行到 Python gec_verify.py 做 1:1 对撞
  473.         if (!_EDGE_GEC_LOG_ONCE || EDGE_CLOCK_SKEW_SECONDS !== 0.0) {
  474.             var dbgLine = '[Edge TTS][GEC] ticks_input=' + java.lang.String.format("%.6f", unixSecFloat)
  475.                         + ' skew=' + EDGE_CLOCK_SKEW_SECONDS.toFixed(4)
  476.                         + ' mod300=' + mod.toFixed(6)
  477.                         + ' ticksLong=' + ticksLong
  478.                         + ' ticksStr_len=' + ticksStr.length
  479.                         + ' src_preview=' + ticksStr.substring(0, Math.min(12, ticksStr.length)) + '...'
  480.                         + ' gec=' + gec;
  481.             print.log(dbgLine);
  482.             _EDGE_GEC_LOG_ONCE = true;
  483.         }

  484.         return gec;
  485.     } catch (e) {
  486.         print.log('[Edge TTS] 生成 Sec-MS-GEC 失败: ' + e);
  487.         return null;
  488.     }
  489. }

  490. function buildSSML(text, voice, rate, volume, pitch) {
  491.     return "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' " +
  492.            "xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='zh-CN'>" +
  493.            "<voice name='" + voice + "'>" +
  494.            "<prosody rate='" + rate + "' volume='" + volume + "' pitch='" + pitch + "'>" +
  495.            escapeXml(text) +
  496.            "</prosody></voice></speak>";
  497. }

  498. /**
  499. * Edge TTS 单次握手尝试(内部函数,供 generateEdge 做自动重试)
  500. * @returns {Object} { ok, code, message, serverDate, output }
  501. */
  502. function tryEdgeOnce(sessionOpts) {
  503.     var text = sessionOpts.text;
  504.     var voice = sessionOpts.voice;
  505.     var rate = sessionOpts.rate;
  506.     var volume = sessionOpts.volume;
  507.     var pitch = sessionOpts.pitch;
  508.     var output = sessionOpts.output;

  509.     var requestId = uuid().replace(/-/g, '');
  510.     var gec = generateSecMsGec();
  511.     var wsUrl = EDGE_TTS_WS_URL
  512.               + '?TrustedClientToken=' + EDGE_TTS_TOKEN
  513.               + '&ConnectionId=' + requestId;
  514.     if (gec) {
  515.         wsUrl += '&Sec-MS-GEC=' + gec
  516.               +  '&Sec-MS-GEC-Version=' + EDGE_TTS_GEC_VERSION;
  517.     }

  518.     var state = {
  519.         connected: false,
  520.         closed: false,
  521.         err: null,
  522.         httpCode: 0,
  523.         serverDate: null,
  524.         audioChunks: [],
  525.         totalLen: 0,
  526.         _loggedFirstFrame: false
  527.     };

  528.     var listener = new WebSocketListener({
  529.         onOpen: function (ws, response) {
  530.             state.connected = true;
  531.             var code = 0;
  532.             try { code = response ? response.code() : 0; } catch(e){}
  533.             print.log('[Edge TTS][WS] ✅ onOpen 已触发,HTTP code=' + code + ' (期望 101 Switching Protocols)');
  534.             try {
  535.                 var xts = (function isoZ() {
  536.                     var d = new Date();
  537.                     var pad = function (n, w) { n = String(n); while (n.length < w) n = '0' + n; return n; };
  538.                     return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1, 2) + '-' + pad(d.getUTCDate(), 2)
  539.                          + 'T' + pad(d.getUTCHours(), 2) + ':' + pad(d.getUTCMinutes(), 2) + ':' + pad(d.getUTCSeconds(), 2) + '.000Z';
  540.                 })();

  541.                 var configMsg = JSON.stringify({
  542.                     context: {
  543.                         synthesis: {
  544.                             audio: {
  545.                                 metadataoptions: { sentenceBoundaryEnabled: false, wordBoundaryEnabled: false },
  546.                                 outputFormat: 'audio-24khz-48kbitrate-mono-mp3'
  547.                             }
  548.                         }
  549.                     }
  550.                 });
  551.                 ws.send('X-Timestamp:' + xts + '\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n' + configMsg);

  552.                 var ssml = buildSSML(text, voice, rate, volume, pitch);
  553.                 ws.send('X-RequestId:' + requestId + '\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:' + xts + '\r\nPath:ssml\r\n\r\n' + ssml);
  554.             } catch (e) {
  555.                 state.err = '发送失败: ' + e;
  556.                 print.log('[Edge TTS][WS] onOpen 内部发送失败: ' + e);
  557.             }
  558.         },

  559.         onMessage: function (ws, message) {
  560.             try {
  561.                 // ========== 1. 兼容 Rhino/OkHttp 多种消息类型 ==========
  562.                 if (message === null || message === undefined) return;
  563.                 // ----------- 文本消息:静默忽略(TEXT 帧是 turn.start / response / audio.metadata / turn.end 等控制消息) -----------
  564.                 var isJavaString = false;
  565.                 try {
  566.                     if (typeof message === 'string') {
  567.                         isJavaString = true;
  568.                     } else if (message && message.getClass) {
  569.                         var cls = message.getClass();
  570.                         if (cls === java.lang.String.class) isJavaString = true;
  571.                         // 再次兜底:如果是 okhttp 传过来的 String(比如某些版本会用
  572.                         // java.lang.CharSequence impl),调用 toString() 之后再检查
  573.                         // 是否全是可打印字符也可以 → 用 instanceof 更稳
  574.                         else if (java.lang.String.class.isInstance(message)) isJavaString = true;
  575.                     }
  576.                 } catch (e_ignore) {}
  577.                 if (isJavaString) return;

  578.                 // 二进制消息:可能是 okhttp3 ByteString (有 toByteArray()),可能是原生 byte[],可能是 Buffer/ByteString 其它
  579.                 var jBytes = null;
  580.                 try {
  581.                     if (typeof message.toByteArray === 'function') {
  582.                         jBytes = message.toByteArray();
  583.                     } else if (typeof message.length === 'number') {
  584.                         // 可能是 JS ArrayBuffer / Uint8Array? AIWROK Rhino 里 byte[] 也有 length 属性
  585.                         var maybeLen = message.length;
  586.                         if (maybeLen === 0 || (maybeLen > 0 && typeof message[0] === 'number')) {
  587.                             // JS Uint8Array 风格,转 Java byte[]
  588.                             jBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, maybeLen);
  589.                             for (var _k = 0; _k < maybeLen; _k++) {
  590.                                 var _bv = message[_k] & 0xff;
  591.                                 jBytes[_k] = (_bv > 127) ? (_bv - 256) : _bv;
  592.                             }
  593.                         } else if (maybeLen > 0 && message.getClass && message.getClass().isArray()) {
  594.                             // 原生 Java byte[]
  595.                             jBytes = message;
  596.                         }
  597.                     }
  598.                 } catch (convErr) {
  599.                     // 转换失败通常是文本控制帧,静默忽略即可
  600.                     return;
  601.                 }
  602.                 if (!jBytes) {
  603.                     // 非二进制消息(文本/其他控制帧),静默忽略
  604.                     return;
  605.                 }

  606.                 var len = jBytes.length;
  607.                 if (len < 2) {
  608.                     print.log('[Edge TTS][bin] frame too small, len=' + len);
  609.                     return;
  610.                 }
  611.                 // ========== 2. 按官方公式:header_length = 前 2 bytes big-endian ==========
  612.                 var headerLength = ((jBytes[0] & 0xff) << 8) | (jBytes[1] & 0xff);
  613.                 // 官方 get_headers_and_data: data[header_length+2:] (这里的 data 就是整个 jBytes)
  614.                 // 所以 header_length 必须 <= len - 2(后面至少留 2 字节给 start index)
  615.                 if (headerLength < 0 || (headerLength + 2) > len) {
  616.                     print.log('[Edge TTS][bin] invalid: headerLength=' + headerLength + ' totalLen=' + len);
  617.                     return;
  618.                 }
  619.                 var audioStart = headerLength + 2;   // 官方 1:1
  620.                 var audioLen = len - audioStart;     // 官方 1:1

  621.                 // ========== 3. 解析 header(官方:line.split(b":", 1),不 strip value) ==========
  622.                 var pathVal = null;
  623.                 var contentTypeVal = null;
  624.                 try {
  625.                     // jBytes[2 : 2 + headerLength] = header bytes
  626.                     var headerArr = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, headerLength);
  627.                     for (var _hi = 0; _hi < headerLength; _hi++) headerArr[_hi] = jBytes[2 + _hi];
  628.                     var headerStr = String(new java.lang.String(headerArr, 'US-ASCII'));
  629.                     var lines = headerStr.split('\r\n');
  630.                     for (var _li = 0; _li < lines.length; _li++) {
  631.                         var ln = lines[_li];
  632.                         if (!ln) continue;
  633.                         var colon = ln.indexOf(':');
  634.                         if (colon === -1) continue;
  635.                         var rawK = ln.substring(0, colon);
  636.                         var rawV = ln.substring(colon + 1);
  637.                         // Rhino String: 正常 key,也可能带 \x00\x80 前缀(如抓包中第一个 key = \x00\x80X-RequestId)
  638.                         // 所以 trim() key 是安全的(去掉\x00这类控制符会更好),然后 toLowerCase
  639.                         var k = rawK.replace(/[\x00-\x1f\x7f]/g, '').trim().toLowerCase();
  640.                         var v = rawV.replace(/[\x00-\x1f\x7f]/g, '').trim().toLowerCase();
  641.                         if (k === 'path') pathVal = v;
  642.                         if (k === 'content-type') contentTypeVal = v;
  643.                     }
  644.                 } catch (parseErr) {
  645.                     print.log('[Edge TTS][bin] header parse error: ' + parseErr);
  646.                     pathVal = null; contentTypeVal = null;
  647.                 }

  648.                 // 日志(不打每帧,只在首次 audio 成功和总计打,避免日志爆炸)
  649.                 if (!state._loggedFirstFrame) {
  650.                     print.log('[Edge TTS][bin] first frame: total=' + len + ' headerLen=' + headerLength + ' audioLen=' + audioLen + ' Path=' + (pathVal||'<NULL>') + ' Content-Type=' + (contentTypeVal||'<NULL>'));
  651.                 }

  652.                 // ========== 4. 按官方判断:Path == 'audio' 且 (Content-Type == 'audio/mpeg' 或 NULL) ==========
  653.                 if (pathVal !== 'audio') {
  654.                     return;  // 非音频帧(例如 WebRTC 心跳等二进制帧)
  655.                 }
  656.                 if (contentTypeVal !== 'audio/mpeg') {
  657.                     // Content-Type 为 NULL 且 data 为空 = 终止帧,直接跳过
  658.                     if (contentTypeVal === null && audioLen === 0) return;
  659.                     // 有 data 但没有 Content-Type = UnexpectedResponse(官方抛异常),这里直接记日志跳过
  660.                     if (contentTypeVal === null) {
  661.                         print.log('[Edge TTS][bin] WARN: Path=audio, no Content-Type but data exists, audioLen=' + audioLen);
  662.                         return;
  663.                     }
  664.                     return;
  665.                 }
  666.                 if (audioLen <= 0) return;

  667.                 // ========== 5. 收集音频 ==========
  668.                 var chunk = new Array(audioLen);
  669.                 for (var kk2 = 0; kk2 < audioLen; kk2++) chunk[kk2] = jBytes[audioStart + kk2] & 0xff;
  670.                 state.audioChunks.push(chunk);
  671.                 state.totalLen += audioLen;
  672.                 if (!state._loggedFirstFrame) {
  673.                     state._loggedFirstFrame = true;
  674.                     var first2 = chunk[0].toString(16) + (chunk[1] < 16 ? '0' : '') + chunk[1].toString(16);
  675.                     print.log('[Edge TTS][bin] first audio bytes: 0x' + first2.toUpperCase() + ' (expect MP3: FFF3/FFF2)');
  676.                 }
  677.             } catch (e) {
  678.                 try { print.log('[Edge TTS] onMessage uncaught error: ' + e); } catch(ee){}
  679.             }
  680.         },

  681.         onClosed: function (ws, code, reason) {
  682.             state.closed = true;
  683.             state.connected = false;
  684.             try { print.log('[Edge TTS][WS] onClosed 触发 code=' + code + ' reason=' + reason); } catch(e){}
  685.         },

  686.         onFailure: function (ws, t, response) {
  687.             state.closed = true;
  688.             state.connected = false;
  689.             state.err = t ? String(t.getMessage()) : 'ws_error';
  690.             try {
  691.                 if (response) {
  692.                     try {
  693.                         state.httpCode = response.code() || 0;
  694.                     } catch (ee) {}
  695.                     try {
  696.                         var hd = response.header('Date');
  697.                         if (hd) state.serverDate = String(hd);
  698.                     } catch (ee) {}
  699.                 }
  700.             } catch (e) {}
  701.             try {
  702.                 print.log('[Edge TTS][WS] ❌ onFailure: err_msg=' + (state.err || '')
  703.                         + ' HTTP_code=' + state.httpCode
  704.                         + ' Date=' + (state.serverDate || '<none>')
  705.                         + ' Throwable_type=' + (t ? String(t.getClass().getName()) : 'null'));
  706.             } catch(e2) {}
  707.         }
  708.     });

  709.     var client = new OkHttpClient.Builder()
  710.         .pingInterval(10, TimeUnit.SECONDS)
  711.         .connectTimeout(25, TimeUnit.SECONDS)
  712.         .readTimeout(40, TimeUnit.SECONDS)
  713.         .build();

  714.     var muid = generateEdgeMuid();
  715.     var edgeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
  716.               + ' (KHTML, like Gecko) Chrome/' + EDGE_CHROMIUM_MAJOR_VERSION + '.0.0.0 Safari/537.36'
  717.               + ' Edg/' + EDGE_CHROMIUM_MAJOR_VERSION + '.0.0.0';

  718.     var request = new Request.Builder()
  719.         .url(wsUrl)
  720.         .addHeader('User-Agent', edgeUA)
  721.         .addHeader('Accept-Encoding', 'gzip, deflate, br, zstd')
  722.         .addHeader('Accept-Language', 'en-US,en;q=0.9')
  723.         .addHeader('Pragma', 'no-cache')
  724.         .addHeader('Cache-Control', 'no-cache')
  725.         .addHeader('Origin', 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold')
  726.         .addHeader('Sec-WebSocket-Version', '13')
  727.         .addHeader('Cookie', 'muid=' + muid + ';')
  728.         .build();

  729.     var ws = client.newWebSocket(request, listener);

  730.     print.log('[Edge TTS][WS] newWebSocket 已发起,等待 onOpen / onFailure / 25s timeout...');

  731.     var w = 0;
  732.     while (!state.connected && !state.err && w < 25000) {
  733.         sleep(100);
  734.         w += 100;
  735.     }
  736.     if (state.err) {
  737.         try { ws.close(1000, 'err'); } catch (e) {}
  738.         try { client.dispatcher().executorService().shutdown(); } catch (e) {}
  739.         var code = 'error';
  740.         if (state.httpCode === 403) code = '403';
  741.         return { ok: false, code: code, message: state.err, serverDate: state.serverDate };
  742.     }
  743.     if (!state.connected) {
  744.         try { ws.close(1000, 'timeout'); } catch (e) {}
  745.         try { client.dispatcher().executorService().shutdown(); } catch (e) {}
  746.         print.log('[Edge TTS][WS] ⏱️  onOpen 等待超时(25秒)未触发,已等待' + (w/1000).toFixed(1) + '秒,err=' + (state.err || '<none>'));
  747.         return { ok: false, code: 'timeout', message: 'WebSocket 连接超时(25秒)', serverDate: state.serverDate };
  748.     }

  749.     w = 0;
  750.     var lastLen = 0;
  751.     var noGrowthMs = 0;
  752.     while (!state.closed && !state.err && w < 45000) {
  753.         sleep(200);
  754.         w += 200;
  755.         if (state.totalLen !== lastLen) {
  756.             lastLen = state.totalLen;
  757.             noGrowthMs = 0;
  758.         } else {
  759.             noGrowthMs += 200;
  760.             if (state.totalLen > 100 && noGrowthMs > 2000) break;
  761.         }
  762.     }
  763.     print.log('[Edge TTS][WS] 接收循环结束:等待=' + (w/1000).toFixed(1) + '秒 audioBytes_total=' + state.totalLen + ' closed=' + state.closed + ' err=' + (state.err||'<none>'));

  764.     try { ws.close(1000, 'done'); } catch (e) {}
  765.     try { client.dispatcher().executorService().shutdown(); } catch (e) {}

  766.     if (state.err) {
  767.         var code2 = 'error';
  768.         if (state.httpCode === 403) code2 = '403';
  769.         return { ok: false, code: code2, message: state.err, serverDate: state.serverDate };
  770.     }
  771.     if (state.totalLen < 100) {
  772.         return { ok: false, code: 'noaudio', message: '未获取到音频数据,请检查网络或参数', serverDate: state.serverDate };
  773.     }

  774.     var merged = new Uint8Array(state.totalLen);
  775.     var offset = 0;
  776.     for (var ci = 0; ci < state.audioChunks.length; ci++) {
  777.         var chunk = state.audioChunks[ci];
  778.         for (var cj = 0; cj < chunk.length; cj++) merged[offset + cj] = chunk[cj];
  779.         offset += chunk.length;
  780.     }

  781.     if (output) {
  782.         if (!writeBinaryFile(output, merged)) {
  783.             return { ok: false, code: 'write', message: '写入文件失败: ' + output, serverDate: state.serverDate };
  784.         }
  785.         print.log('[Edge TTS] 已保存: ' + output + ' (' + state.totalLen + ' 字节)');
  786.         return { ok: true, output: output, serverDate: state.serverDate };
  787.     }
  788.     return { ok: true, output: merged, serverDate: state.serverDate };
  789. }

  790. /**
  791. * Edge TTS 核心生成(同步阻塞,关键字接口)—— 带官方 DRM 时钟偏移自动校准+重试
  792. *
  793. * @param {Object} options
  794. * @param {string} options.text       - 要合成的文字(必填)
  795. * @param {string} [options.voice]    - 音色ID,默认 zh-CN-XiaoxiaoNeural
  796. * @param {string} [options.rate]     - 语速 "+0%" / "-20%" / "+50%"
  797. * @param {string} [options.volume]   - 音量 "+0%" / "+50%"
  798. * @param {string} [options.pitch]    - 音调 "+0Hz" / "-10Hz"
  799. * @param {string} [options.output]   - 输出路径(.mp3),不填返回 Uint8Array
  800. * @returns {string|Uint8Array|null}  成功返回路径或 Uint8Array,失败返回 null
  801. */
  802. function generateEdge(options) {
  803.     var opts = options || {};
  804.     if (!opts.text || !String(opts.text).trim()) {
  805.         print.log('[Edge TTS] 缺少必填参数: text');
  806.         return null;
  807.     }
  808.     var sessionOpts = {
  809.         text: opts.text,
  810.         voice: opts.voice || DEFAULT_EDGE_VOICE,
  811.         rate: opts.rate || '+0%',
  812.         volume: opts.volume || '+0%',
  813.         pitch: opts.pitch || '+0Hz',
  814.         output: opts.output || null
  815.     };

  816.     // 第 1 次尝试
  817.     var r = tryEdgeOnce(sessionOpts);
  818.     if (r.ok) return r.output;

  819.     // 官方 DRM.handle_client_response_error:
  820.     // 若失败且带 Date 头 → 计算 skew = serverDate - localNow → 修正 skew → 自动重试
  821.     if (r.code === '403' && r.serverDate) {
  822.         print.log('[Edge TTS][403] 收到服务器 Date 头: ' + String(r.serverDate));
  823.         var serverTs = parseRfc2616Date(r.serverDate);
  824.         var localTsNow = java.lang.System.currentTimeMillis() / 1000.0;
  825.         if (serverTs !== null) {
  826.             var skew = serverTs - localTsNow;
  827.             print.log('[Edge TTS][403] serverTs_parsed=' + serverTs.toFixed(3)
  828.                      + ' localNow=' + localTsNow.toFixed(3)
  829.                      + ' skew=' + skew.toFixed(3));
  830.             var oldSkew = EDGE_CLOCK_SKEW_SECONDS;
  831.             EDGE_CLOCK_SKEW_SECONDS += skew;
  832.             print.log('[Edge TTS] 校准时钟偏差: old_skew=' + oldSkew.toFixed(3)
  833.                      + ' +delta=' + skew.toFixed(3)
  834.                      + ' → new_skew=' + EDGE_CLOCK_SKEW_SECONDS.toFixed(3) + ' 秒(403 后自动重试)');
  835.             var r2 = tryEdgeOnce(sessionOpts);
  836.             if (r2.ok) return r2.output;
  837.             print.log('[Edge TTS] 重试后仍失败: code=' + r2.code + ' msg=' + r2.message
  838.                     + (r2.serverDate ? ' serverDate=' + r2.serverDate : ''));
  839.             return null;
  840.         } else {
  841.             print.log('[Edge TTS][403] 日期解析失败!无法计算时钟偏差,放弃重试');
  842.         }
  843.     }

  844.     // 无 Date 头或非 403 错误,直接打印
  845.     print.log('[Edge TTS] 失败(' + r.code + '): ' + r.message);
  846.     return null;
  847. }

  848. // ==================== 豆包 VoiceGenie TTS 核心 ====================

  849. var DOUBAO_AUDIO_CONFIG = { bit_rate: 32000, format: 'ogg_opus', sample_rate: 24000 };

  850. /**
  851. * 豆包 VoiceGenie TTS 核心生成(同步阻塞,关键字接口)
  852. */
  853. function generateDoubao(options) {
  854.     var opts = options || {};
  855.     var cookies = opts.cookies;
  856.     var conversationId = opts.conversationId;
  857.     var messageId = opts.messageId;
  858.     var questionId = opts.questionId || null;
  859.     var sectionId = opts.sectionId || '0';
  860.     var speaker = opts.speaker || DEFAULT_DOUBAO_VOICE;
  861.     var speechRate = opts.speechRate || 1.0;
  862.     var pitch = opts.pitch || 0;
  863.     var audioConfig = opts.audioConfig || DOUBAO_AUDIO_CONFIG;
  864.     var output = opts.output || null;

  865.     if (!cookies) { print.log('[豆包 TTS] 缺少必填参数: cookies'); return null; }
  866.     if (!conversationId) { print.log('[豆包 TTS] 缺少必填参数: conversationId'); return null; }
  867.     if (!messageId) { print.log('[豆包 TTS] 缺少必填参数: messageId'); return null; }

  868.     var cookieStr = cookieObjToString(cookies);
  869.     var qId = questionId || messageId;
  870.     var sessionOpts = {
  871.         conversationId: conversationId, questionId: qId, messageId: messageId,
  872.         sectionId: sectionId, speaker: speaker, speechRate: speechRate,
  873.         pitch: pitch, audioConfig: audioConfig, cookieStr: cookieStr
  874.     };

  875.     var state = {
  876.         connected: false,
  877.         closed: false,
  878.         sessionId: null,
  879.         gotTTSEnded: false,
  880.         audioChunks: [],
  881.         totalLen: 0,
  882.         err: null
  883.     };

  884.     var wsUrl = buildDoubaoWsUrl(cookieStr);

  885.     var listener = new WebSocketListener({
  886.         onOpen: function (ws, response) {
  887.             state.connected = true;
  888.             try {
  889.                 ws.send(ByteString.of(toJavaBytes(buildRequestFrame('StartTask', '{}', null))));
  890.                 ws.send(ByteString.of(toJavaBytes(buildRequestFrame('Ping', '{}', null))));
  891.             } catch (e) {
  892.                 state.err = '发送 StartTask 失败: ' + e;
  893.             }
  894.         },

  895.         onMessage: function (ws, message) {
  896.             try {
  897.                 if (typeof message === 'string') return;
  898.                 if (!message || !message.toByteArray) return;

  899.                 var jBytes = message.toByteArray();
  900.                 var u8 = new Uint8Array(jBytes.length);
  901.                 for (var i = 0; i < jBytes.length; i++) u8[i] = jBytes[i] & 0xff;

  902.                 var parsed = parseResponseFrame(u8);
  903.                 var ev = parsed.eventType;

  904.                 if (ev === 'TaskStarted') {
  905.                     var payload1 = buildDoubaoSessionPayload(sessionOpts);
  906.                     ws.send(ByteString.of(toJavaBytes(buildRequestFrame('StartSession', payload1, null))));

  907.                 } else if (ev === 'SessionStarted') {
  908.                     state.sessionId = uuid();
  909.                     ws.send(ByteString.of(toJavaBytes(buildRequestFrame('Ping', '{}', state.sessionId))));
  910.                     var payload2 = buildDoubaoSessionPayload(sessionOpts);
  911.                     ws.send(ByteString.of(toJavaBytes(buildRequestFrame('StartTTS', payload2, state.sessionId))));

  912.                 } else if (ev === 'TTSResponse') {
  913.                     if (parsed.audioData && parsed.audioData.length > 0) {
  914.                         state.audioChunks.push({ seq: parsed.seq, data: parsed.audioData });
  915.                         state.totalLen += parsed.audioData.length;
  916.                     }

  917.                 } else if (ev === 'TTSEnded') {
  918.                     state.gotTTSEnded = true;
  919.                     try { ws.close(1000, 'done'); } catch (e) {}
  920.                 }
  921.             } catch (e) {}
  922.         },

  923.         onClosed: function (ws, code, reason) {
  924.             state.closed = true;
  925.             state.connected = false;
  926.         },

  927.         onFailure: function (ws, t, response) {
  928.             state.closed = true;
  929.             state.connected = false;
  930.             state.err = t ? String(t.getMessage()) : 'ws_error';
  931.         }
  932.     });

  933.     var client = new OkHttpClient.Builder()
  934.         .pingInterval(10, TimeUnit.SECONDS)
  935.         .connectTimeout(15, TimeUnit.SECONDS)
  936.         .readTimeout(60, TimeUnit.SECONDS)
  937.         .build();

  938.     var request = new Request.Builder()
  939.         .url(wsUrl)
  940.         .addHeader('Cookie', cookieStr)
  941.         .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')
  942.         .addHeader('Origin', 'https://www.doubao.com')
  943.         .build();

  944.     var ws = client.newWebSocket(request, listener);

  945.     var w = 0;
  946.     while (!state.connected && !state.err && w < 15000) {
  947.         sleep(100);
  948.         w += 100;
  949.     }
  950.     if (state.err) {
  951.         print.log('[豆包 TTS] 连接失败: ' + state.err);
  952.         try { ws.close(1000, 'err'); } catch (e) {}
  953.         try { client.dispatcher().executorService().shutdown(); } catch (e) {}
  954.         return null;
  955.     }
  956.     if (!state.connected) {
  957.         print.log('[豆包 TTS] WebSocket 连接超时 (15秒)');
  958.         try { ws.close(1000, 'timeout'); } catch (e) {}
  959.         try { client.dispatcher().executorService().shutdown(); } catch (e) {}
  960.         return null;
  961.     }

  962.     w = 0;
  963.     var lastLen = 0;
  964.     var noGrowthMs = 0;
  965.     while (!state.closed && !state.gotTTSEnded && !state.err && w < 60000) {
  966.         sleep(200);
  967.         w += 200;
  968.         if (state.totalLen !== lastLen) {
  969.             lastLen = state.totalLen;
  970.             noGrowthMs = 0;
  971.         } else {
  972.             noGrowthMs += 200;
  973.             if (state.totalLen > 0 && noGrowthMs > 3000) break;
  974.         }
  975.     }

  976.     try { ws.close(1000, 'done'); } catch (e) {}
  977.     try { client.dispatcher().executorService().shutdown(); } catch (e) {}

  978.     if (state.err) {
  979.         print.log('[豆包 TTS] ' + state.err);
  980.         return null;
  981.     }
  982.     if (state.audioChunks.length === 0) {
  983.         print.log('[豆包 TTS] 未收到任何音频数据,请检查 cookies/对话参数');
  984.         return null;
  985.     }

  986.     state.audioChunks.sort(function (a, b) { return a.seq - b.seq; });
  987.     var merged = new Uint8Array(state.totalLen);
  988.     var offset = 0;
  989.     for (var ci = 0; ci < state.audioChunks.length; ci++) {
  990.         var data = state.audioChunks[ci].data;
  991.         for (var cj = 0; cj < data.length; cj++) {
  992.             merged[offset + cj] = data[cj];
  993.         }
  994.         offset += data.length;
  995.     }

  996.     if (output) {
  997.         if (writeBinaryFile(output, merged)) {
  998.             print.log('[豆包 TTS] 已保存: ' + output + ' (' + state.totalLen + ' 字节)');
  999.             return output;
  1000.         }
  1001.         print.log('[豆包 TTS] 写入文件失败: ' + output);
  1002.         return null;
  1003.     }
  1004.     return merged;
  1005. }

  1006. // ==================== 直接运行示例 ====================

  1007. (function runDemo() {
  1008.     print.log('=== AIWROK TTS CORE 单文件版 ===');
  1009.     print.log('时间: ' + new Date().toString());
  1010.     print.log('');

  1011.     // --- Edge TTS 示例 ---
  1012.     print.log('[1] Edge TTS 生成中...');
  1013.     var demoText = '庚子年后京城的买卖不好做,前门大街倒了三家茶馆,唯独德顺的王掌柜,日子反倒越过越舒展。'
  1014.         + '熟客们都打趣,说老王是祖坟冒了青烟,平白无故的好运往怀里撞。'
  1015.         + '王掌柜有个旁人瞧不上的小习惯:每天天刚蒙蒙亮,伙计还没上门,他先搬条凳坐柜台后头,'
  1016.         + '就着油灯的光,在麻纸本子上写半页字。写的不是什么圣贤文章,全是鸡零狗碎的闲事:'
  1017.         + '张二爷爱喝焖得酽的香片,李三赊了俩铜子儿的茶钱,驴打滚剩了八块卖不动,街对过杂货铺的东家爱喝茉莉双熏。'
  1018.         + '常来蹭茶喝的混混儿刘四笑他:“王掌柜,你这是要考秀才啊?开茶馆卖水得了,费那洋劲干嘛?”'
  1019.         + '王掌柜也不恼,嘿嘿一笑,把本子收进抽屉,第二天照旧写。'
  1020.         + '那年春上茶叶价涨得邪乎,别家茶馆要么涨价把客人挤走,要么换碎茶末子砸招牌。'
  1021.         + '唯独德顺,价没大涨,茶味还跟从前一样地道。满街的人都说老王运气好,提前囤着好茶了。'
  1022.         + '哪是运气?他那半页纸上,早记着“每年开春茶价必涨三成,头年秋里定货最稳”。'
  1023.         + '这不是他天生会算,是天天写、年年记,摸出来的老规矩。旁人年年临渴掘井,他早把井挖在了前头。'
  1024.         + '还有一回,有个南下的绸缎商来城里收贷,进茶馆歇脚。王掌柜一眼就想起,'
  1025.         + '前阵子本子上记过这位客官爱喝碧螺春,还托人打听过京里靠谱的绸缎庄。'
  1026.         + '他一边上茶一边顺嘴提了两家字号,连东家的脾气秉性都顺带说了。'
  1027.         + '那商人感激不尽,往后带了好几拨同行来,成了固定的大主顾。'
  1028.         + '旁人又说,老王会来事,平白撞上了贵人。其实哪是撞的?是他天天记着客人的喜好,机会递到跟前,伸手就接住了。'
  1029.         + '日子久了,德顺的生意越做越稳,别家倒闭的伙计都来投奔。'
  1030.         + '有人专门来讨教“转运”的法子,王掌柜就把那本麻纸本子翻出来给人看:'
  1031.         + '“哪有什么好运?我就是每天早起写半页纸,写着写着,账也清了,人也熟了,该避的坑避了,该抓的机会也没跑。”'
  1032.         + '私塾的陈先生呷了口茶,慢悠悠补了句:“这就叫拱心石的习惯。看着是每天半页纸的小事,'
  1033.         + '实则把记性、眼力、心思都串起来了。习惯成了本能,就不用天天费脑子琢磨运气。'
  1034.         + '提示是开门点灯,动作是记账识人,甜头是买卖顺当,日子一长,好运就成了自然而然的事儿。”'
  1035.         + '也有人不服气,买了本子回去学,没半个月就嫌麻烦,扔在了墙角。'
  1036.         + '照旧是稀里糊涂过日子,赔了赚了都不知道根由,遇着不顺心就怨命不好。'
  1037.         + '王掌柜后来常跟伙计说,天底下的好运,从来不是天上掉下来的。'
  1038.         + '就像这茶馆的门轴,天天擦着润着,开门就顺当;锈住了,再使劲也推不开。'
  1039.         + '每天半页纸,就是那点润轴的油,看着不起眼,日子久了,就能撑起整份日子的安稳。';
  1040.     var edgeOut = generateEdge({
  1041.         text: demoText,
  1042.         voice: DEFAULT_EDGE_VOICE,
  1043.         rate: '+0%',
  1044.         volume: '+0%',
  1045.         pitch: '+0Hz',
  1046.         output: '/sdcard/edge_tts_demo.mp3'
  1047.     });
  1048.     if (edgeOut) {
  1049.         print.log('    生成成功: ' + edgeOut);
  1050.     } else {
  1051.         print.log('    生成失败');
  1052.     }
  1053.     print.log('');

  1054.     // --- 豆包 TTS 示例(需真实参数,否则会失败) ---
  1055.     print.log('[2] 豆包 TTS 示例(需填入真实参数才能运行)...');
  1056.     var doubaoOut = generateDoubao({
  1057.         cookies: 'sessionid=YOUR_SESSIONID; session_ss=YOUR_SESSION_SS',
  1058.         conversationId: 'YOUR_CONVERSATION_ID',
  1059.         messageId: 'YOUR_MESSAGE_ID',
  1060.         speaker: DEFAULT_DOUBAO_VOICE,
  1061.         speechRate: 1.0,
  1062.         output: '/sdcard/doubao_tts_demo.ogg'
  1063.     });
  1064.     if (doubaoOut) {
  1065.         print.log('    生成成功: ' + doubaoOut);
  1066.     } else {
  1067.         print.log('    预期失败(参数未填)');
  1068.     }

  1069.     // --- 打印音色列表 ---
  1070.     print.log('');
  1071.     print.log('=== 可用音色 ===');
  1072.     print.log('Edge TTS (' + EDGE_VOICES.length + ' 个):');
  1073.     for (var i = 0; i < EDGE_VOICES.length; i++) {
  1074.         print.log('  ' + EDGE_VOICES[i].id + '  [' + EDGE_VOICES[i].name + ']');
  1075.     }
  1076.     print.log('');
  1077.     print.log('豆包 TTS (' + DOUBAO_VOICES.length + ' 个):');
  1078.     for (var j = 0; j < DOUBAO_VOICES.length; j++) {
  1079.         print.log('  ' + DOUBAO_VOICES[j].id + '  [' + DOUBAO_VOICES[j].name + ']');
  1080.     }
  1081. })();

  1082. exit();
复制代码


untoAIWROK软件自动文字转语音例子nextnocontent
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

相关导读了
    采集亚马逊正版群发工具有没有?
    Apr.20旅行X心语今天来说说YYPOST新功能的一个灵活用法,采集亚马逊商品信息,并且获得排名的软件,亚马逊现在越来越多客户做,淘宝的水是越来越清了,以前做电商的客户,现在都转战到外国,最赚钱的要数一些客户往亚马逊里堆了吧,拿我这个YYPOST的客户,最多的是采集,分析排名,刷价格,刷数量,改价,刷访问量等等技术

    企业发展B2B网站有什么东东软件可以发呢
    标题企业发展网B2B软件,现在虽然B2B网站收录不错,可愁的是心急的人们,他们太想一口吃撑胖子了,发帖宣传虽然不能像佛系那样淡定,但也不能像跑火车那般急躁对待,自己内容不收录,完全是自己操作内容问题,可以参考一下别人的内容是怎么弄的,然后自己要试着转变,而且收录这个内容,常常会变化的,不是一种规则就吃到老

    搜房天下房聊软件哪一个好呢
    本帖最后由 发帖软件 于 2019-5-22 16:15 编辑 2搜房天下群发房聊信息软件,开始本来打算做58同城的,但发一个就要一次点触验证码,这就让人没有感觉到存在的价值了吧,都是卖二手房和新房的搜房天下倒是可以发即时聊天信息,也没有发现他这个网站有啥子限制,登陆一个搜房天下账号,然后采集回来分类列表的网址,然后就一

    大家坛有没有好用的群发工具下载呢
    当你的笑容给我礼貌的招呼,大家坛全自动发帖软件,宣传推广是一场持久战,总是有一些人把软件用了一天,或是几个小时,就觉得自己付出太多了,那加进来的粉丝,或是流量,应该是和宣传多少成正比的,其实没有这么便宜的事,就像很多阅读量超过一百万的视频,或是电影,真正会在屏幕打赏的人不会超过三千,真正大额打赏给主

    群发正版软件中国塑料网
    中国塑料网群发软件YYPOST脚本下载地址,这个网站会有一个很奇怪的问题就是你在首页登陆无半个验证码,但在登陆网址登陆就会有一个验证码,所以我们灵活一些,在首页登陆就不用输入验证码了哈。网站秒收录比较高,但发的都是五金和建筑行业,先前有很多人都是发土建工程的大公司操作的,现在这个网站专为那个行业诞生的吧。

    OpenStreetMap网站正版2019年发帖工具下载
    本帖最后由 发帖软件 于 2019-5-21 11:13 编辑 OpenStreetMap网站全自动群发,OpenStreetMapOpenStreetMap(简称OSM,中文是公开地图)是一个网上地图协作计划,目标是创造一个内容自由且能让所有人编辑的世界地图。有的人编辑地图然后等收录,有的人发日志等收录,我们这里也是利用地图日志做为宣传的目标,简单的脚本理

    搜房天下全自动收短信全自动识别验证码注册账号软件
    房天下自动注册机,这个脚本是前几天发房聊的脚本廷伸品种,这个脚本能做到自动注册账号,自动保存账号,自动发房聊的效果,不过今天我们主要说一说怎么注册账号写脚本吧,这个搜房天天下的账号,可以发提问,可以发房聊,发论坛,发博客,还有发个人中心页都是有秒收的效果的,这样就省去了去买号,去乱花钱的效果了吧,而

    企业邮箱安卓端有什么APP软件可以发的呢
    请输入标题企业邮箱安卓发发送邮箱脚本,这个脚本是利用企业邮箱进行群发的,全程是一种模拟手工操作的过程,所以封号是很少的,而且企业邮箱群发到普通QQ邮箱不容易进垃圾箱中的,所以这个脚本也是这样的原理,不过最好是利用一些多开器,登陆多点的QQ邮箱账号会比较流畅一些,然后用软件一个一个的切换APP进行群发邮件会

    头条留评论软件有没有好用的呢?
    今天整一个今日头条留言软件,对于留言YYPOST是优势是比较大的存在,因为他往往专注一些下拉定位的优点,像今日头条这样,还是需要一些特殊下拉定位的,因为他新闻有长有短,有图有视频的,所以综合起来定位是比较难的,如果用POST也不是很轻松可以破解他的加密参数。这个脚本也是有一个不好的地方就是换号会比较麻烦,您电

    单网页生成神器
    最近新技术,网页生成机占领了整个网络的半壁江山,效果很疯狂,虽然不知道能持续多久,作为开发软件的领头者,一直在找收录的方法,一直在努力创新着,一直被人模仿,却从没有被超越过,这个网页生成机,已经出来有一段时间了,一直没有拿出来分享,醉过醉过,它是利用的一些小小收录漏洞整的,您最好用一些老站域名,进行

关闭
快速回复 返回列表 返回顶部
本站自动发贴软件,是现在最流行的做脚本软件,这种发贴工具,不但发贴收录快,而且抢占好的先机,完全自由编辑,实现针对性群发模拟操作,软件可以顶贴,也可以发贴,可以兼容支持Discuz、PHPWind、Dvbbs三大主流论坛,有手机验证码收件,邮件收发的功能,支持验证码识别,注册问题识别,多线程任务,自动上传头像,自动激活注册邮件,兼容防注册插件,本站软件原创正版,更新效率最快的原创软件。 『网络推广软件』『自动发帖软件』『 自动发帖