信息发布软件,b2b软件,广告发布软件

 找回密码
 立即注册
搜索
查看: 4|回复: 0
打印 上一主题 下一主题

[宣传软件网站动态] 语音合成核心模块AIWROK 单文件版

[复制链接]

1952

主题

1962

帖子

1万

积分

积分
11440
跳转到指定楼层
宣传软件楼主
发表于 昨天 07:46 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

资讯缩略图:

资讯发布日期:2026-08-10

资讯发布简介:语音合成核心模块AIWROK 单文件版

资讯关键词:语音合成核心模块AIWROK 单文件版

资讯所属分类:IT资讯 SEO资讯 

联系:

① 本信息收集于网络,如有不对的地方欢迎联系我纠正!
② 本信息免费收录,不存在价格的问题!
③ 如果您的网站也想这样出现在这里,请您加好友情链接,我当天会审核通过!

④友情链接关键字:软件网站分类目录 网址:http://www.postbbs.com/

资讯详细描述
语音合成核心模块AIWROK 单文件版
语音合成核心模块AIWROK 单文件版 b2b软件
  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软件自动文字转语音例子next极速视频APP刷金币
回复

使用道具 举报

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

本版积分规则

相关导读
群发软件极速视频APP刷金币
极速视频APP刷金币
群发软件语音合成核心模块AIWROK 单文件版
语音合成核心模块AIWROK 单文件版
群发软件AIWROK软件自动文字转语音例子
AIWROK软件自动文字转语音例子
群发软件AIWROK adb.exec() 实用工具库 —— 常用功能直接调用版
AIWROK adb.exec() 实用工具库 —— 常用功能直接调用版
群发软件安卓AIWROK获取包名所有方法汇总
安卓AIWROK获取包名所有方法汇总
群发软件AIWROK软件番茄小说自动阅读刷金币示例
AIWROK软件番茄小说自动阅读刷金币示例
信息发布软件AIWROK软件无碍模式一键删除所有APP版示例
AIWROK软件无碍模式一键删除所有APP版示例
信息发布软件AIWROK软件无碍模式auto模块抖音极速版滑动刷视频
AIWROK软件无碍模式auto模块抖音极速版滑动刷视频
信息发布软件快手极速版自动刷视频无障碍模式
快手极速版自动刷视频无障碍模式
信息发布软件AIWROK软件代理模式一键删除桌面所有APP版示例
AIWROK软件代理模式一键删除桌面所有APP版示例
信息发布软件AIWROK软件自动找图利用agent代理模式删除应用版示例
AIWROK软件自动找图利用agent代理模式删除应用版示例
信息发布软件解析Kimi返回的拖动距离示例
解析Kimi返回的拖动距离示例
信息发布软件自动截图并发送给DEEPSEEK识别示例过程
自动截图并发送给DEEPSEEK识别示例过程
信息发布软件AIWROK软件测试文心识别识别验证码示例
AIWROK软件测试文心识别识别验证码示例
信息发布软件元宝截图B站验证码示例测试
元宝截图B站验证码示例测试
信息发布软件示例千问自动采集文章保存记事本
示例千问自动采集文章保存记事本
信息发布软件豆包识别验证码加拖动验证码示例
豆包识别验证码加拖动验证码示例
信息发布软件豆包对话采集示例
豆包对话采集示例
信息发布软件启动自动百度贴吧发图片
启动自动百度贴吧发图片
信息发布软件自动百度贴吧示例
自动百度贴吧示例
信息发布软件代理模式简书自动流程
代理模式简书自动流程
信息发布软件启动极速版任务页看小说赚钱自动阅读
启动极速版任务页看小说赚钱自动阅读
信息发布软件agent代理模式功能小结
agent代理模式功能小结
信息发布软件完整苹果IOS小脚本文本输入与粘贴流程
完整苹果IOS小脚本文本输入与粘贴流程
信息发布软件AIWROK使用activity列表开关与进度条的协同交互
AIWROK使用activity列表开关与进度条的协同交互
群发软件苹果脚本找图后Detect对象各种方法演示
苹果脚本找图后Detect对象各种方法演示
群发软件苹果脚本网络API监控与数据管理工具
苹果脚本网络API监控与数据管理工具
群发软件IOS苹果脚本TXT文件读写H5日志打印完整示例
IOS苹果脚本TXT文件读写H5日志打印完整示例
群发软件AIWROK软件演示多线程创建暂停恢复
AIWROK软件演示多线程创建暂停恢复
群发软件AIWROK软件定时检查页面状态防止卡死技巧
AIWROK软件定时检查页面状态防止卡死技巧
群发软件安卓脚本runTime相关功能示例
安卓脚本runTime相关功能示例
群发软件floatUI实例秒显示秒关闭吐司功能
floatUI实例秒显示秒关闭吐司功能
群发软件苹果脚本HID多行实例粘帖板示例
苹果脚本HID多行实例粘帖板示例
群发软件安卓脚本HID节点查找完整方法示例
HID节点查找完整方法示例
群发软件AIWROK软件框架的抖音自动化营销系统
AIWROK软件框架的抖音自动化营销系统
群发软件赛博朋克任务控制中心H5示例
赛博朋克任务控制中心H5示例
群发软件苹果脚本现代深色UI设计风格
现代深色UI设计风格
群发软件AIWROK软件智能任务监控悬浮窗
AIWROK软件智能任务监控悬浮窗
群发软件AIWROK软件事件监听示例
AIWROK软件事件监听示例
群发软件QZ掘金原生UI界面
QZ掘金原生UI界面
群发软件安卓脚本auto方法完整示例
安卓脚本auto方法完整示例
群发软件安卓脚本暂停弹出确认对话框
安卓脚本暂停弹出确认对话框
群发软件苹果iOS脚本天气信息查询工具
苹果iOS脚本天气信息查询工具
群发软件安卓脚本简化版H5日志输出窗口
安卓脚本简化版H5日志输出窗口
群发软件安卓脚本简化版H5日志输出窗口
安卓脚本简化版H5日志输出窗口
群发软件安卓脚本应用重置、代理模式监控及补丁升级
安卓脚本应用重置、代理模式监控及补丁升级
群发软件安卓脚本里的打开关闭安装以及卡密授权信息
安卓脚本里的打开关闭安装以及卡密授权信息
群发软件AIWROK软件脚本GET下载文件并检测0KB工具
AIWROK软件脚本GET下载文件并检测0KB工具
群发软件安卓脚本HID按键方法完整示例
安卓脚本HID按键方法完整示例
群发软件AIWROK软件键鼠HID滑动所有方法功能演示
AIWROK软件键鼠HID滑动所有方法功能演示
群发软件苹果脚本里HID的5种滑动方法的使用
苹果脚本里HID的5种滑动方法的使用
群发软件苹果脚本示例7种不同的OCR识别和点击方法
苹果脚本示例7种不同的OCR识别和点击方法
群发软件AIWROK软件安卓脚本FTP上传下载例子
AIWROK软件安卓脚本FTP上传下载例子
群发软件AIWROK软件脚本JSON转换示例
AIWROK软件脚本JSON转换示例
群发软件AIWROK软件运算符高级应用实例
AIWROK软件运算符高级应用实例
群发软件JavaScript语法小示例
JavaScript语法小示例
群发软件展示JavaScript各种语句标识符的实际应用
展示JavaScript各种语句标识符的实际应用
群发软件AIWROK条件语句实用示例智能活动推荐系统
AIWROK条件语句实用示例智能活动推荐系统
群发软件AIWROK软件示例数组方法实战应用
AIWROK软件示例数组方法实战应用
群发软件示例JavaScript的 try-catch-finally-throw用法
示例JavaScript的 try-catch-finally-throw用法
群发软件JavaScript 高级错误处理实战示例
JavaScript 高级错误处理实战示例
群发软件安卓脚本AIWROK软件示例JS函数高级用法
安卓脚本AIWROK软件示例JS函数高级用法
群发软件AIWROK环境JavaScript编码规范完整示例
AIWROK环境JavaScript编码规范完整示例
群发软件安卓脚本示例实时截图判断页面变化
安卓脚本示例实时截图判断页面变化
群发软件苹果脚本7种不同的OCR识别
苹果脚本7种不同的OCR识别

QQ|( 京ICP备09078825号 )

本网站信息发布软件,是可以发布论坛,发送信息到各大博客,各大b2b软件自动发布,好不夸张的说:只要手工能发在电脑打开IE能发的网站,用这个宣传软件就可以仿制动作,进行推送发到您想发送的B2B网站或是信息发布平台上,不管是后台,还是前台,都可以进行最方便的广告发布,这个广告发布软件,可以按月购买,还可以试用软件,对网站的验证码也可以完全自动对信息发布,让客户自动找上门,使企业轻松实现b2b发布,这个信息发布软件,均是本站原创正版开发,拥有正版的血统,想要新功能,欢迎提意见给我,一好的分类信息群发软件在手,舍我其谁。QQ896757558

GMT+8, 2026-8-11 09:01 , Processed in 0.293802 second(s), 52 queries .

宣传软件--信息发布软件--b2b软件广告发布软件

快速回复 返回顶部 返回列表