 | |  |  | 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[0] & 0xFF, b1 = h[1] & 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[i];
- 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*['"]([a-zA-Z0-9:_\-]+)['"]/i,
- /var\s+token\s*=\s*['"]([a-zA-Z0-9:_\-]+)['"]/i,
- /["']token["']\s*[:=]\s*["']([a-zA-Z0-9:_\-]+)["']/i
- ];
- for (var i = 0; i < tokenPatterns.length; i++) {
- var m = html.match(tokenPatterns[i]);
- if (m && m[1]) { token = m[1]; break; }
- }
- // 站点2可能需要 nonce(WordPress)
- var nonce = "";
- var noncePatterns = [
- /["']nonce["']\s*[:=]\s*["']([a-f0-9]{6,32})["']/i,
- /nonce\s*[:=]\s*["']([a-f0-9]{6,32})["']/i
- ];
- for (var i = 0; i < noncePatterns.length; i++) {
- var m = html.match(noncePatterns[i]);
- if (m && m[1]) { nonce = m[1]; 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[1] != "200") {
- printl(" ↳ API 返回错误: " + jsonStr.substring(0, 200));
- return null;
- }
- var dlMatch = jsonStr.match(/"download"\s*:\s*"([^"]+)"/);
- if (dlMatch && dlMatch[1]) return dlMatch[1];
- 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[s];
- // 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("🔊 [text-to-speech.cn] 音色:" + voice + " 共 " + segs.length + " 段");
- var okCount = 0;
- for (var i = 0; i < segs.length; i++) {
- var seg = segs[i];
- 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[i];
- 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[r];
- var seg = ARTICLE[idx];
- 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(); // 朗读裕泰茶馆全文
复制代码
| |  | |  |
|