YYPOST群发软件 发表于 5 天前

AIWROK软件自动文字转语音例子

AIWROK软件自动文字转语音例子

/**
* ============================================================================
*AIWROK TTS 文件⑤:text-to-speech.cn(微软 Azure 神经网络音色·免登录)
* ============================================================================
*来源:https://www.text-to-speech.cn/
*      网站后端代理微软 Azure TTS,音色和 Edge-TTS 完全一样
*      但用 HTTP POST 代替 WebSocket,AIWROK 兼容性更好
*
*音质:⭐⭐⭐⭐⭐和豆包桃子一样自然,无机器感
*
*中文女声推荐:
*    zh-CN-XiaoxiaoNeural   晓晓最自然(默认)
*    zh-CN-XiaochenNeural   晓辰年轻女声
*    zh-CN-XiaorouNeural    晓柔温柔
*    zh-CN-XiaohanNeural    晓涵温柔
*    zh-CN-XiaomengNeural   晓梦年轻女声
*    zh-CN-XiaoyiNeural   晓伊女童
*中文男声:
*    zh-CN-YunxiNeural      云希年轻男声
*    zh-CN-YunjianNeural    云健成年男声
*
*用法:
*    ttsWeb("你好呀");
*    ttsWeb("你好呀", "zh-CN-XiaorouNeural");// 晓柔
*
*文件保存:/sdcard/Pictures/aiwrok_tts/
* ============================================================================
*/

importClass(Packages.android.media.MediaPlayer);
importClass(Packages.java.io.File);
importClass(Packages.java.io.FileInputStream);
importClass(Packages.java.io.FileOutputStream);
importClass(Packages.java.lang.Thread);
importClass(Packages.javax.net.ssl.HttpsURLConnection);
importClass(Packages.java.net.URL);
importClass(Packages.java.io.BufferedReader);
importClass(Packages.java.io.InputStreamReader);
importClass(Packages.java.io.OutputStream);

var WEB_CFG = {
    cacheDir: "/sdcard/Pictures/aiwrok_tts/",
    // 默认音色:晓晓(最自然)
    voice: "zh-CN-XiaoxiaoNeural",
    // 语言值必须是中文全称(网站要求)
    language: "中文(普通话,简体)",
    // 音质:audio-16khz-32kbitrate-mono-mp3 / audio-24khz-160kbitrate-mono-mp3 / audio-48khz-192kbitrate-mono-mp3
    kbitrate: "audio-24khz-160kbitrate-mono-mp3",
    // 语速 / 音调
    rate: "0",
    pitch: "0",
    // 音量(0-100)
    volume: "75",
    // 情感:0=默认
    style: "0",
    styledegree: "1",
    // 其他参数
    role: "0",
    silence: "",
    predict: "0",
    // 验证码(网站固定值)
    yzm: "202410170001",
    // 单次最大字数
    maxLen: 200,
    // 两个站点(互为备用)
    sites: [
      {
            page: "https://www.text-to-speech.cn/",
            api: "https://www.text-to-speech.cn/getSpeek.php"
      },
      {
            page: "http://new.text-to-speech.cn/tts/",
            api: "http://new.text-to-speech.cn/wp-content/plugins/speech-tts/getSpeek.php"
      }
    ]
};

function ensureDir() {
    var d = new File(WEB_CFG.cacheDir);
    if (!d.exists()) d.mkdirs();
}

function isValidAudio(p) {
    try {
      var f = new File(p);
      if (!f.exists() || f.length() < 512) return false;
      var fis = new FileInputStream(p);
      var h = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 3);
      var r = fis.read(h);
      fis.close();
      if (r < 2) return false;
      var b0 = h & 0xFF, b1 = h & 0xFF;
      if (b0 == 0x49 && b1 == 0x44) return true;
      if (b0 == 0xFF && (b1 & 0xE0) == 0xE0) return true;
      return false;
    } catch (e) { return false; }
}

function splitText(text, maxLen) {
    var segs = [];
    if (!text) return segs;
    text = String(text);
    var parts = text.split(/([。!?!?,,;;\n\r])/);
    var cur = "";
    for (var i = 0; i < parts.length; i++) {
      var p = parts;
      if (p == null) continue;
      if (cur.length + p.length > maxLen) {
            if (cur.length > 0) { segs.push(cur); cur = ""; }
            if (p.length > maxLen) {
                for (var j = 0; j < p.length; j += maxLen) {
                  segs.push(p.substring(j, Math.min(j + maxLen, p.length)));
                }
            } else { cur = p; }
      } else { cur += p; }
    }
    if (cur.length > 0) segs.push(cur);
    return segs;
}

/**
* 从页面 HTML 中提取 nonce 和 token
*/
function getPageAuth(pageUrl) {
    var conn = null;
    var reader = null;
    try {
      var url = new URL(pageUrl);
      conn = url.openConnection();
      conn.setRequestMethod("GET");
      conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
      conn.setConnectTimeout(15000);
      conn.setReadTimeout(30000);
      conn.setUseCaches(false);

      var code = conn.getResponseCode();
      if (code != 200) return null;

      reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
      var sb = new java.lang.StringBuilder();
      var line;
      while ((line = reader.readLine()) != null) {
            sb.append(line).append("\n");
      }

      var html = String(sb.toString());

      // 提取 token(网站在 script 标签里用 const token = 'xxx' 定义)
      var token = "";
      var tokenPatterns = [
            /const\s+token\s*=\s*['"](+)['"]/i,
            /var\s+token\s*=\s*['"](+)['"]/i,
            /["']token["']\s*[:=]\s*["'](+)["']/i
      ];
      for (var i = 0; i < tokenPatterns.length; i++) {
            var m = html.match(tokenPatterns);
            if (m && m) { token = m; break; }
      }

      // 站点2可能需要 nonce(WordPress)
      var nonce = "";
      var noncePatterns = [
            /["']nonce["']\s*[:=]\s*["']({6,32})["']/i,
            /nonce\s*[:=]\s*["']({6,32})["']/i
      ];
      for (var i = 0; i < noncePatterns.length; i++) {
            var m = html.match(noncePatterns);
            if (m && m) { nonce = m; break; }
      }

      return { nonce: nonce, token: token };
    } catch (e) {
      printl("↳ 获取页面认证信息失败: " + e.message);
      return null;
    } finally {
      if (reader != null) { try { reader.close(); } catch (e) {} }
      if (conn != null) { try { conn.disconnect(); } catch (e) {} }
    }
}

/**
* POST 到 getSpeek.php 获取 TTS 音频
*/
function ttsApiCall(apiUrl, text, voice, auth) {
    var conn = null;
    var reader = null;
    var os = null;
    try {
      // 构建表单数据(参数名和值必须与网站完全一致)
      var params = "language=" + encodeURIComponent(WEB_CFG.language) +
                     "&voice=" + encodeURIComponent(voice) +
                     "&text=" + encodeURIComponent(text) +
                     "&role=" + WEB_CFG.role +
                     "&style=" + WEB_CFG.style +
                     "&rate=" + WEB_CFG.rate +
                     "&pitch=" + WEB_CFG.pitch +
                     "&kbitrate=" + encodeURIComponent(WEB_CFG.kbitrate) +
                     "&silence=" + WEB_CFG.silence +
                     "&styledegree=" + WEB_CFG.styledegree +
                     "&volume=" + WEB_CFG.volume +
                     "&predict=" + WEB_CFG.predict +
                     "&user_id=" +
                     "&yzm=" + WEB_CFG.yzm +
                     "&replice=1";

      if (auth && auth.nonce) params += "&nonce=" + auth.nonce;
      if (auth && auth.token) params += "&token=" + encodeURIComponent(auth.token);

      var url = new URL(apiUrl);
      conn = url.openConnection();
      conn.setRequestMethod("POST");
      conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
      conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
      conn.setRequestProperty("Accept", "application/json, text/plain, */*");
      conn.setRequestProperty("Referer", apiUrl.replace("/getSpeek.php", "/"));
      conn.setDoOutput(true);
      conn.setDoInput(true);
      conn.setConnectTimeout(15000);
      conn.setReadTimeout(30000);
      conn.setUseCaches(false);

      // 发送请求体(Rhino 的 JS String 没有 getBytes,要用 java.lang.String)
      os = conn.getOutputStream();
      var jParams = new java.lang.String(params);
      os.write(jParams.getBytes("UTF-8"));
      os.flush();
      os.close();
      os = null;

      var code = conn.getResponseCode();
      var inputStream;
      if (code >= 400) {
            inputStream = conn.getErrorStream();
      } else {
            inputStream = conn.getInputStream();
      }

      if (inputStream == null) return null;

      reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
      var sb = new java.lang.StringBuilder();
      var line;
      while ((line = reader.readLine()) != null) {
            sb.append(line);
      }

      var response = String(sb.toString());
      return { code: code, body: response };
    } catch (e) {
      printl("↳ API 请求失败: " + e.message);
      return null;
    } finally {
      if (os != null) { try { os.close(); } catch (e) {} }
      if (reader != null) { try { reader.close(); } catch (e) {} }
      if (conn != null) { try { conn.disconnect(); } catch (e) {} }
    }
}

/**
* 从 JSON 响应中提取下载 URL
*/
function parseDownloadUrl(jsonStr) {
    try {
      // Rhino 的 JSON.parse 可能不可用,用正则提取
      // 格式: {"code":200,"download":"https://...mp3","srt":""}
      var codeMatch = jsonStr.match(/"code"\s*:\s*(\d+)/);
      if (!codeMatch || codeMatch != "200") {
            printl("↳ API 返回错误: " + jsonStr.substring(0, 200));
            return null;
      }

      var dlMatch = jsonStr.match(/"download"\s*:\s*"([^"]+)"/);
      if (dlMatch && dlMatch) return dlMatch;

      return null;
    } catch (e) {
      printl("↳ 解析响应失败: " + e.message);
      return null;
    }
}

/**
* 下载 MP3 文件(带重试)
*/
function downloadMp3(url, savePath) {
    for (var retry = 0; retry < 3; retry++) {
      try {
            new File(savePath)["delete"]();
            var client = new okHttp();
            client.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
            client.downloadFile(url, savePath);
            if (isValidAudio(savePath)) return true;
      } catch (e) {
            Thread.sleep(1000);
      }
      if (retry < 2) Thread.sleep(1000);
    }
    return false;
}

/**
* 合成单段文字(多次重试,401时等待后刷新token)
*/
function synthSegment(text, voice, savePath) {
    for (var retry = 0; retry < 4; retry++) {
      for (var s = 0; s < WEB_CFG.sites.length; s++) {
            var site = WEB_CFG.sites;

            // 1. 每次都重新获取 token(token 会过期)
            var auth = getPageAuth(site.page);
            if (!auth) {
                continue;
            }

            // 2. 调用 API
            var resp = ttsApiCall(site.api, text, voice, auth);
            if (!resp || resp.code != 200) {
                continue;
            }

            // 3. 解析下载 URL
            var dlUrl = parseDownloadUrl(resp.body);
            if (!dlUrl) {
                // 401 错误:token 过期,等 3 秒再试
                if (resp.body && resp.body.indexOf("401") >= 0) {
                  printl("↳ token 过期,等待 3 秒后重试...");
                  Thread.sleep(3000);
                }
                continue;
            }

            // 4. 下载 MP3
            if (downloadMp3(dlUrl, savePath)) {
                return true;
            }
      }
      // 重试前等待
      if (retry < 3) {
            var wait = (retry + 1) * 2000; // 2s, 4s, 6s 递增
            printl("↳ 第 " + (retry + 1) + " 次重试(等待 " + (wait / 1000) + "s)...");
            Thread.sleep(wait);
      }
    }

    return false;
}

/**
* 文字转语音(主函数)
* @param text文本
* @param voice 音色名(默认晓晓)
* @return 成功段数
*/
function ttsWeb(text, voice) {
    voice = voice || WEB_CFG.voice;
    if (!text || String(text).length === 0) { printl("⚠️ 文本为空"); return 0; }
    ensureDir();

    var segs = splitText(text, WEB_CFG.maxLen);
    printl("&#128266; 音色:" + voice + " 共 " + segs.length + " 段");

    var okCount = 0;
    for (var i = 0; i < segs.length; i++) {
      var seg = segs;
      var tag = seg.substring(0, 10).replace(/[\\/:*?"<>|]/g, "_");
      var savePath = WEB_CFG.cacheDir + "web_" + voice.replace(/[^a-zA-Z0-9]/g, "").substring(0, 15) +
                     "_" + (i + 1) + "_" + Date.now() + ".mp3";

      printl("[" + (i + 1) + "/" + segs.length + "] " + seg.substring(0, 25) + "...");

      if (synthSegment(seg, voice, savePath)) {
            var f = new File(savePath);
            printl("✅ " + f.length() + " bytes");
            okCount++;
      } else {
            printl("❌ 合成失败");
      }

      if (i < segs.length - 1) Thread.sleep(1500);
    }

    printl("✅ 完成 (" + okCount + "/" + segs.length + " 段)");
    return okCount;
}

// ======================== 演示 ========================

function demo() {
    printl("========================================");
    printl("⑤ text-to-speech.cn TTS 演示");
    printl("微软 Azure 神经网络音色·免登录");
    printl("========================================");

    // 测试短句
    printl("\n【晓晓 - 默认】");
    ttsWeb("你好呀,我是晓晓,一个温暖文静的声音。今天的风很温柔,适合慢慢聊天。");

    Thread.sleep(500);

    printl("\n【晓柔 - 温柔】");
    ttsWeb("你好,我是晓柔,说话轻轻的,像在你耳边讲故事一样。", "zh-CN-XiaorouNeural");

    Thread.sleep(500);

    printl("\n【晓辰 - 年轻女声】");
    ttsWeb("你好,我是晓辰,一个阳光的年轻女孩声音,很高兴认识你哦。", "zh-CN-XiaochenNeural");

    Thread.sleep(500);

    printl("\n【云希 - 年轻男声】");
    ttsWeb("你好,我是云希,一个年轻男声,希望你喜欢。", "zh-CN-YunxiNeural");

    printl("\n演示结束。MP3 都在 /sdcard/Pictures/aiwrok_tts/ 目录下。");
}

// ======================== 裕泰茶馆全文朗读 ========================

function readArticle() {
    var ARTICLE = [
      "光绪末年,北京的裕泰茶馆的秋阳总斜着扫过八仙桌,茶碗里的茉莉浮浮沉沉。掌柜王利发攥着布巾擦柜台,眼睛瞅着门,耳朵却撒得满茶馆都是,这地方,三教九流聚聚着,闲话里藏着买卖,叹气里裹着门路。",
      "这天晌午,绸缎庄的秦二爷一屁股坐下,茶没端就叹气:晦气,新到的苏杭绸缎压了半仓,这年头兵荒马乱的,谁还有闲钱做什么新衣裳呀,再撂俩月,怕是要生霉喂虫子去了。",
      "话音刚落,跑码头的李把头掀帘子进来,灌了大碗凉茶:您那愁?我从南边运来了百斤好药材,找不着下家,码头堆一天扣一天的钱,再耗下去,我得把裤腰带当出去算了。",
      "角落开饭庄的张胖子也拍着大腿搭腔:都别喊苦了,我那饭庄刚换了金字招牌,想给伙计做身整齐的衣裳,还想寻点正经好羊肉做招牌菜,跑了半月,没着落呀,眼看就要开张了,急得一满嘴是泡。",
      "王利发手里擦着茶碗,慢悠悠的插了话:唉,我当多大的难处,秦二爷,您那绸缎,张掌柜正愁没处买,张掌柜您要的羊肉,街西口卖肉的马大哥,昨儿还在我这念叨囤了好货出不去,李把头您那药材,对过德仁掌柜,前儿还托我打听南边的新货哪有。",
      "三句话落地,仨人先是愣了神,紧跟着眼睛都亮了,当场就凑到一桌子,您让几分利,我担点运费,没半个时辰,三桩买卖全说妥了,秦二爷清了库存,李把头卸包袱,张胖子凑了开张的家当,个个喜颜开,临走每人都塞给王利发一块碎银子,说是喝茶的钱。",
      "靠窗坐着的孙秀才,穿得打补丁长衫,天天来茶馆蹭座位抄书,张嘴就是寒窗苦读,凭真才实学立身,这会儿他推了推破眼镜,酸溜溜的说,张掌柜,您这算是什么本事?不过是耍耍嘴皮子牵线,投机取巧罢了,哪像我,日日苦读,靠的是真本事吃饭,才踏实呀。",
      "王利发也不恼,给他添了半碗热茶,笑着说:孙先生,您努力是真的,可您细想哦,这天下的本事,您学得完吗?您既不会织绸缎,也不会跑码头,边挑羊肉都不会,总不能样样都自己攥在手里吧?",
      "我这小茶馆,赚得不光是茶钱,也是一个方便的钱,谁手里有闲货,谁心里有难处,我听着记着,到时候两头凑到一块儿,大家都省了力气,我也落个中间费,您天天闷头抄书,总想着自己成天大的事儿,可连街对面的药铺缺啥药材都不知道,这努力有啥用,不就走歪了跑。",
      "孙秀才张了张嘴,半天没说出话来。",
      "转眼过了半年,秦二爷的绸缎庄开了分号,李把头的货栈雇了仨伙计,张胖子的饭庄成了京城小有名气的馆子。仨人逢年过节都来给王利发送礼,裕泰茶馆也翻修了门面,比原先敞亮不少。",
      "只有孙秀才,还是天天抱着旧书抄,嘴里念叨着怀才不遇,长衫上的补丁反倒又多了两块。",
      "后来有人跟王利发讨教发财的诀窍,他总摆着手笑:哪有什么诀窍?我既没金山也没银矿,就是认得人多,知道谁有啥、谁缺啥,给人搭个桥罢了。这世上的钱,从来不是攥在自己手里才叫钱。你要是能成了那搭桥的人,各色价值都得从你这儿过,财富哪里还用追?它自然就往你这儿流。",
      "茶客们听了,有的点头,有的撇嘴,茶碗盖一掀,茉莉花香又飘满了一屋子。"
    ];

    printl("========================================");
    printl("裕泰茶馆的搭桥经 · text-to-speech.cn");
    printl("音色:晓晓(微软 Azure 神经网络)");
    printl("========================================");
    printl("文章共 " + ARTICLE.length + " 段");

    ensureDir();
    var okCount = 0;
    var voice = "zh-CN-XiaoxiaoNeural";
    var failed = []; // 记录失败的段落

    for (var i = 0; i < ARTICLE.length; i++) {
      var seg = ARTICLE;
      var savePath = WEB_CFG.cacheDir + "web_xiaoxiao_seg" + (i + 1) + ".mp3";

      // 已下载的跳过
      var existFile = new File(savePath);
      if (existFile.exists() && existFile.length() > 512) {
            printl("\n[" + (i + 1) + "/" + ARTICLE.length + "] 已存在,跳过 (" + existFile.length() + " bytes)");
            okCount++;
            continue;
      }

      printl("\n[" + (i + 1) + "/" + ARTICLE.length + "] " + seg.substring(0, 25) + "...");

      if (synthSegment(seg, voice, savePath)) {
            var f = new File(savePath);
            printl("✅ " + f.length() + " bytes");
            okCount++;
      } else {
            printl("❌ 合成失败,稍后重试");
            failed.push(i);
      }

      // 段间间隔 2 秒(避免频繁请求导致 token 过期)
      if (i < ARTICLE.length - 1) Thread.sleep(2000);
    }

    // 第二轮:重试失败的段落
    if (failed.length > 0) {
      printl("\n========================================");
      printl("第二轮:重试 " + failed.length + " 个失败段落");
      printl("========================================");

      for (var r = 0; r < failed.length; r++) {
            var idx = failed;
            var seg = ARTICLE;
            var savePath = WEB_CFG.cacheDir + "web_xiaoxiao_seg" + (idx + 1) + ".mp3";

            printl("\n[重试 " + (r + 1) + "/" + failed.length + "] 段落 " + (idx + 1) + ": " + seg.substring(0, 25) + "...");

            // 等待 5 秒让 token 冷却
            Thread.sleep(5000);

            if (synthSegment(seg, voice, savePath)) {
                var f = new File(savePath);
                printl("✅ " + f.length() + " bytes");
                okCount++;
            } else {
                printl("❌ 仍然失败");
            }
      }
    }

    printl("\n========================================");
    printl("完成 (" + okCount + "/" + ARTICLE.length + " 段)");
    printl("MP3 保存在: " + WEB_CFG.cacheDir);
    printl("========================================");
}

// 运行:取消注释你想执行的函数

// demo();         // 演示 4 种音色
readArticle();       // 朗读裕泰茶馆全文

页: [1]
查看完整版本: AIWROK软件自动文字转语音例子