牛津高阶10 TTS,近期全平台失效问题的讨论

分享一下我后来搭的TTS链接,和上面k兄的替换方法一致

解决方案2 :js中搜索speech.platform.bing.com 替换为public.fanqi987.dpdns.org/public/wss/speech.platform.bing.com

感谢分享!能用。

Hip, hip, hooray!

感谢分享,可以用。

实测,深蓝,可用!谢谢!!!

[quote=“fanqi987, post:44, topic:42406”]
public.fanqi987.dpdns.org/public/wss/speech.platform.bing.com
实测了,原来两个灰的小喇叭都可以朗读,感谢 :clap:

dictango(电脑端)为什么可以朗读……是因为用的edge webview还是自己实现了tts..

真好!真强!感谢!

平板上实测可用,感谢:folded_hands:

请问修改这个地址需要梯子吗?没有梯子能用吗

不需要梯子啊

Everything’s running smoothly as of 04.09.26! Thanks a lot!

又失效了。顺问对EdgeTTS现在有可靠的websocket fetch方法吗?手机app貌似只支持这种

使用国内引擎让TTS本地化,这个可行性如何

PC上容易解决,手机上较难,有很多限制。

TTS 签名算法升级为 GEC v2,可以叫AI改一下

Deepseek帮改后还是没有效果

OALD10:将 Edge TTS 签名升级为 GEC v2

适用范围

适用于 oald.js 中使用 createEdgeTTS()、generateSecMsGecToken(),并携带旧版 Sec-MS-GEC-Version=1-130.0.2849.68 的 OALD10 版本。

本文代码取自已部署脚本。签名算法移植自 edge-tts 7.3.1 的 edge_tts/drm.py。这是对当时接口的兼容修复,不保证微软后续修改协议后仍永久有效。

只修改两处:签名函数、协议版本。不要只改版本号。 公共代理地址、音色、音频解析、界面和 CSS 均无需改动。

一、备份原文件

先备份词典目录中的 oald.js,例如另存为 oald.before-gecv2.js。用支持 UTF-8 的代码编辑器打开原文件,尽量保留原来的换行符。

若文件已经使用本文的完整 GEC v2 函数及版本号,无须重复修改。

二、替换整个签名函数

在 createEdgeTTS() 内找到:

function generateSecMsGecToken() {
    // 原来的签名代码
}

将整个函数(包括函数体及末尾花括号)替换为下面的完整代码。保留后面原有的 async function ensureSocketReady(),不要一并删除。

此函数继续使用 createEdgeTTS() 内原有的 TRUSTED_CLIENT_TOKEN 常量。无需添加 Python 服务或额外的 JS 文件。

function generateSecMsGecToken() {
    // GEC v2; port of edge-tts DRM. All arithmetic is modulo 2^32.
    var C = {"initial":[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],"state":[4255104674,2435071207,3192492180,3442351626,1811967817,79508907,4272990180,66059218],"schedule":[3945054110,358115615,2696978146,217723578,2787307148,3520959633,3235718560,4192668342,4039316851,1884868174,1904797190,15051571,3529027680,1920361828],"rounds":[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],"adjustment":3386640039,"coefficients":[[3895366065,3005677776],[1155409094,2296378653],[1005282748,693190712],[3365689651,3005795295],[2367603217,3557143765],[827258325,652566332],[3911487921,1618477510],[4037392853,2192753147],[101076211,4199654362],[3665427040,545047652],[3201434976,1010595059],[2314848760,3536931681],[905590982,3137643201],[3410617286,3311559015]]};
    function rol(v) { return ((v << 16) | (v >>> 16)) >>> 0; }
    function rr(v,n) { return (v >>> n) | (v << (32-n)); }
    function s0(v) { return (rr(v,7)^rr(v,18)^(v>>>3))>>>0; }
    function s1(v) { return (rr(v,17)^rr(v,19)^(v>>>10))>>>0; }
    function expand(w,start) {
        for(var i=start;i<64;i++) w[i]=(s1(w[i-2])+w[i-7]+s0(w[i-15])+w[i-16])>>>0;
    }
    function rounds(state,w,first) {
        var a=state[0],b=state[1],c=state[2],d=state[3],e=state[4],f=state[5],g=state[6],h=state[7];
        for(var i=first;i<64;i++) {
            var k=(first===14 && i===14)?C.adjustment:C.rounds[i];
            // Explicit unsigned conversions: JS XOR returns a signed int32.
            var t1=(h+((rr(e,6)^rr(e,11)^rr(e,25))>>>0)+((g^(e&(f^g)))>>>0)+k+w[i])>>>0;
            var t2=(((rr(a,2)^rr(a,13)^rr(a,22))>>>0)+(((a&b)|(c&(a|b)))>>>0))>>>0;
            h=g;g=f;f=e;e=(d+t1)>>>0;d=c;c=b;b=a;a=(t1+t2)>>>0;
        }
        return [a,b,c,d,e,f,g,h];
    }
    function sum(a,b) { return a.map(function(x,i){return (x+b[i])>>>0;}); }
    var seconds=Math.floor((Date.now()/1000+11644473600)/300)*300;
    var text=String(BigInt(seconds)*BigInt(10000000))+TRUSTED_CLIENT_TOKEN;
    var data=Array.prototype.map.call(text,function(x){return x.charCodeAt(0);});
    function be(bytes,p) { return ((bytes[p]<<24)|(bytes[p+1]<<16)|(bytes[p+2]<<8)|bytes[p+3])>>>0; }
    function le(bytes,p) { return ((bytes[p]||0)|((bytes[p+1]||0)<<8)|((bytes[p+2]||0)<<16)|((bytes[p+3]||0)<<24))>>>0; }
    var w=Array(14).fill(0);
    w.push((data[0]<<16)|(data[1]<<8)|data[2]);w.push(be(data,3));
    for(var i=16;i<30;i++) {
        var v=C.schedule[i-16];
        if(i-2>=14) v+=i===16?(s1(w[i-2])^0x000B92C0)>>>0:s1(w[i-2]);
        if(i-7>=14) v+=w[i-7];
        if(i-15>=14) v+=i===29?(s0(w[i-15])^0x059C0B80)>>>0:s0(w[i-15]);
        w.push(v>>>0);
    }
    w.push((s1(w[28])+w[23]+s0(w[15])+w[14]+C.adjustment-C.rounds[14])>>>0);
    expand(w,31);
    var state=sum(C.initial,rounds(C.state,w,14));
    var length=57+data.length,padded=data.slice(7);padded.push(128);
    var zeroes=((55-length)%64+64)%64;
    while(zeroes-->0)padded.push(0);
    var bits=length*8;
    padded.push(0,0,0,0,(bits>>>24)&255,(bits>>>16)&255,(bits>>>8)&255,bits&255);
    for(var p=0;p<padded.length;p+=64) {
        w=[];for(var j=0;j<16;j++)w.push(be(padded,p+j*4));
        expand(w,16);state=sum(state,rounds(state,w,0));
    }
    var digest=[];
    state.forEach(function(v){digest.push(v>>>24,(v>>>16)&255,(v>>>8)&255,v&255);});
    var keyA=(le(digest,0)|1)>>>0,keyB=(le(digest,4)|1)>>>0;
    var factors=[Math.imul(keyA,0x82893933)>>>0,Math.imul(keyB,0x82893933)>>>0];
    var terms=[],offsets=[];
    C.coefficients.forEach(function(pair,i){terms.push(Math.imul(pair[0],factors[i%2])>>>0);offsets.push(pair[1]);});
    var wordsData=[0].concat(data);
    var count=Math.ceil(wordsData.length/4);if(count%2)count++;
    for(i=0;i<count;i++) {
        var word=le(wordsData,i*4),term=Math.imul(word,i%2?keyB:keyA),offset=Math.imul(rol(word),i%2?0xC31713DB:0xEF0569FB);
        if(i===0){term+=Math.imul(0xF2F90B1A,factors[0]);offset+=0x0B1A0000;}
        terms.push(term>>>0);offsets.push(offset>>>0);
    }
    var swapAcc=0,swapLast=0,csState=0,acc=0,t,q;
    for(i=0;i<terms.length;i+=2) {
        var x=terms[i],y=terms[i+1];
        t=rol((offsets[i]+x)>>>0);q=Math.imul(t,0x76F50F34)>>>0;
        swapAcc+=(Math.imul(rol(q),0xC3EFEA97)+q)>>>0;
        t=rol((offsets[i+1]+y)>>>0);q=Math.imul(t,0x1881CE3C)>>>0;
        swapLast=(Math.imul(rol(q),0x35BD1EC9)+q)>>>0;swapAcc=(swapAcc+swapLast)>>>0;
        csState=(Math.imul(csState,keyA)+x)>>>0;
        [0xCF98B111,0x87085B9F,0x12CEB96D,0x257E1D83].forEach(function(k){csState=Math.imul(rol(csState),k)>>>0;});
        acc+=csState;t=(Math.imul(csState,keyB)+y)>>>0;
        t=Math.imul(rol(t),0xA27416F5)>>>0;t=Math.imul(rol(t),0xD38396FF)>>>0;
        csState=Math.imul(rol(Math.imul(rol(t),0x7C932B89)>>>0),0xBFA49F69)>>>0;
        acc=(acc+csState)>>>0;
    }
    function hex(bytes){return bytes.map(function(v){return ('0'+v.toString(16)).slice(-2);}).join('').toUpperCase();}
    var cs=[];
    [(swapLast^csState)>>>0,(swapAcc^acc)>>>0].forEach(function(v){cs.push(v&255,(v>>>8)&255,(v>>>16)&255,v>>>24);});
    return hex(cs)+'-'+hex(digest);
}

说明

  • 时间按 5 分钟窗口取整。
  • BigInt 保证 Windows 时间刻度计算精确。
  • Math.imul() 和 >>> 0 保证 32 位整数计算与参考实现一致。
  • 新版签名包含定制 SHA-256 与 CS64,不能用普通 CryptoJS.SHA256() 直接替代。
  • 输出格式为 <16位大写十六进制>-<64位大写十六进制>。

三、修改协议版本号

在 ensureSocketReady() 中找到:

const Sec_MS_GEC_VERSION = "1-130.0.2849.68";

改为:

const Sec_MS_GEC_VERSION = "2-143.0.3650.75";

修改后的连接部分应保留如下结构:

const Sec_MS_GEC = generateSecMsGecToken();
const Sec_MS_GEC_VERSION = "2-143.0.3650.75";

socket = new WebSocket(
    `${SYNTH_URL}&Sec-MS-GEC=${Sec_MS_GEC}&Sec-MS-GEC-Version=${Sec_MS_GEC_VERSION}`
);

四、保留原公共代理地址

本次修复不需要切换代理。原配置保持:

const TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
const SYNTH_URL = `wss://public.fanqi987.dpdns.org/public/wss/speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}`;

这里的 TRUSTED_CLIENT_TOKEN 是客户端使用的固定协议常量,不是用户私人 API Key。不要把自建代理的私人访问令牌写进公开教程。

原有的 CryptoJS 加载代码可以保留,不要顺带删除其他依赖。

五、保存与检查

  1. 保存 oald.js,确保文件名和词典原来引用的一致。
  2. 如可使用 Node.js,检查语法:
node --check oald.js
  1. 完全退出欧路词典并重新打开,避免旧脚本缓存。
  2. 找到支持在线 TTS 的例句,分别点击英音与美音喇叭。
  3. 再点第二条例句,检查连续请求是否正常。

无需重新打包 MDX/MDD,也不需要修改 CSS。 本教程针对词典已从外部加载该 oald.js 的部署方式;若使用其他封装方式,需要先确认实际加载的脚本位置。

六、本次验证结果与边界

  • 签名算法:108 组 Python / JavaScript 时间戳对照结果一致。
  • Minis WebKit:实际 OALD10 TTS 函数经原公共代理合成成功。
  • 英音 Ryan:返回 31,798 字节音频。
  • 美音 Jenny:返回 19,990 字节音频,同一连接复用成功。
  • 浏览器测试对 play() 使用测试替身,验证了真实联网合成和原音频解析,不等同于欧路真机已实际发声。最终播放效果需在欧路内确认。

七、排错与回滚

仍无法朗读

  • 确认完整替换了函数,而不是只改版本号。
  • 确认没有粘贴 Markdown 的三反引号到 JS 文件中。
  • 确认欧路加载的是修改后的 oald.js,而不是同名旧副本。
  • 检查设备日期和时间;签名依赖当前时间。
  • 公共代理、网络或微软上游故障也会造成失败,不能仅凭“无法播放”认定签名错误。

回滚

用备份的原脚本恢复 oald.js,完全退出欧路后重开即可。注意:回滚只撤销修改,不保证旧签名仍能被微软接受。

大神,牛!目前可用。

感觉又到折腾没底的模式了,试了这方法,还是没效果。

在原来的js代理换成收费的,其他未变,马上就OK,侧面证明同这个关系不大