feat(workers): 添加 workers 源码
This commit is contained in:
13
workers/package.json
Normal file
13
workers/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "wstrans",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"start": "wrangler dev"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"wrangler": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
226
workers/src/index.js
Normal file
226
workers/src/index.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
const encoder = new TextEncoder();
|
||||||
|
let expiredAt = null;
|
||||||
|
let endpoint = null;
|
||||||
|
let clientId = '76a75279-2ffa-4c3d-8db8-7b47252aa41c';
|
||||||
|
|
||||||
|
async function handleRequest(request) {
|
||||||
|
const requestUrl = new URL(request.url);
|
||||||
|
const path = requestUrl.pathname;
|
||||||
|
|
||||||
|
if (path === '/tts') {
|
||||||
|
const text = requestUrl.searchParams.get('t') || '';
|
||||||
|
const voiceName = requestUrl.searchParams.get('v') || 'zh-CN-XiaoxiaoMultilingualNeural';
|
||||||
|
const rate = Number(requestUrl.searchParams.get('r')) || 0;
|
||||||
|
const pitch = Number(requestUrl.searchParams.get('p')) || 0;
|
||||||
|
const outputFormat = requestUrl.searchParams.get('o') || 'audio-24khz-48kbitrate-mono-mp3';
|
||||||
|
const download = requestUrl.searchParams.get('d') || false;
|
||||||
|
const response = await getVoice(text, voiceName, rate, pitch, outputFormat, download);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(path === '/voices') {
|
||||||
|
const l = (requestUrl.searchParams.get('l') || '').toLowerCase();
|
||||||
|
const f = requestUrl.searchParams.get('f');
|
||||||
|
let response = await voiceList();
|
||||||
|
|
||||||
|
if(l.length > 0) {
|
||||||
|
response = response.filter(item => item.Locale.toLowerCase().includes(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(f === "0") {
|
||||||
|
response = response.map(item => {
|
||||||
|
return `
|
||||||
|
- !!org.nobody.multitts.tts.speaker.Speaker
|
||||||
|
avatar: ''
|
||||||
|
code: ${item.ShortName}
|
||||||
|
desc: ''
|
||||||
|
extendUI: ''
|
||||||
|
gender:${item.Gender === 'Female' ? '0' : '1'}
|
||||||
|
name: ${item.LocalName}
|
||||||
|
note: 'wpm: ${item.WordsPerMinute||''}'
|
||||||
|
param: ''
|
||||||
|
sampleRate: ${item.SampleRateHertz|| '24000'}
|
||||||
|
speed: 1.5
|
||||||
|
type: 1
|
||||||
|
volume: 1`
|
||||||
|
})
|
||||||
|
return new Response(response.join('\n'), headers={
|
||||||
|
'Content-Type': 'application/html; charset=utf-8'
|
||||||
|
});
|
||||||
|
}else if(f === "1"){
|
||||||
|
const map = new Map(response.map(item => [item.ShortName, item.LocalName]))
|
||||||
|
return new Response(JSON.stringify(Object.fromEntries(map)), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else {
|
||||||
|
return new Response(JSON.stringify(response), {
|
||||||
|
headers:{
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = request.url.split('://')[0] + "://" +requestUrl.host;
|
||||||
|
return new Response(`
|
||||||
|
<ol>
|
||||||
|
<li> /tts?t=[text]&v=[voice]&r=[rate]&p=[pitch]&o=[outputFormat] <a href="${baseUrl}/tts?t=hello, world&v=zh-CN-XiaoxiaoMultilingualNeural&r=0&p=0&o=audio-24khz-48kbitrate-mono-mp3">try</a> </li>
|
||||||
|
<li> /voices?l=[locate, like zh|zh-CN]&f=[format, 0/1/empty 0(TTS-Server)|1(MultiTTS)] <a href="${baseUrl}/voices?l=zh&f=1">try</a> </li>
|
||||||
|
</ol>
|
||||||
|
`, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }});
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener('fetch', event => {
|
||||||
|
event.respondWith(handleRequest(event.request));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getEndpoint() {
|
||||||
|
const endpointUrl = 'https://dev.microsofttranslator.com/apps/endpoint?api-version=1.0';
|
||||||
|
const headers = {
|
||||||
|
'Accept-Language': 'zh-Hans',
|
||||||
|
'X-ClientVersion': '4.0.530a 5fe1dc6c',
|
||||||
|
'X-UserId': '0f04d16a175c411e',
|
||||||
|
'X-HomeGeographicRegion': 'zh-Hans-CN',
|
||||||
|
'X-ClientTraceId': clientId,
|
||||||
|
|
||||||
|
'X-MT-Signature': await sign(endpointUrl),
|
||||||
|
'User-Agent': 'okhttp/4.5.0',
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Content-Length': '0',
|
||||||
|
'Accept-Encoding': 'gzip'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(endpointUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers
|
||||||
|
}).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sign(urlStr) {
|
||||||
|
const url = urlStr.split('://')[1];
|
||||||
|
const encodedUrl = encodeURIComponent(url);
|
||||||
|
const uuidStr = uuid();
|
||||||
|
const formattedDate = dateFormat();
|
||||||
|
const bytesToSign = `MSTranslatorAndroidApp${encodedUrl}${formattedDate}${uuidStr}`.toLowerCase();
|
||||||
|
const decode = await base64ToBytes('oik6PdDdMnOXemTbwvMn9de/h9lFnfBaCWbGMMZqqoSaQaqUOqjVGm5NqsmjcBI1x+sS9ugjB55HEJWRiFXYFw==');
|
||||||
|
const signData = await hmacSha256(decode, bytesToSign);
|
||||||
|
const signBase64 = await bytesToBase64(signData);
|
||||||
|
return `MSTranslatorAndroidApp::${signBase64}::${formattedDate}::${uuidStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateFormat() {
|
||||||
|
const formattedDate = new Date().toUTCString().replace(/GMT/, '').trim() + 'GMT';
|
||||||
|
return formattedDate.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVoice(text, voiceName = 'zh-CN-XiaoxiaoMultilingualNeural', rate = 0, pitch = 0, outputFormat='audio-24khz-48kbitrate-mono-mp3', download=false) {
|
||||||
|
// get expiredAt from endpoint.t (jwt token)
|
||||||
|
if (!expiredAt || Date.now() / 1000 > expiredAt - 60) {
|
||||||
|
endpoint = await getEndpoint();
|
||||||
|
const jwt = endpoint.t.split('.')[1];
|
||||||
|
const decodedJwt = JSON.parse(atob(jwt));
|
||||||
|
expiredAt = decodedJwt.exp;
|
||||||
|
const seconds = (expiredAt - Date.now() / 1000);
|
||||||
|
clientId = uuid();
|
||||||
|
console.log('getEndpoint, expiredAt:' + (seconds/ 60) + 'm left')
|
||||||
|
} else {
|
||||||
|
const seconds = (expiredAt - Date.now() / 1000);
|
||||||
|
console.log('expiredAt:' + (seconds/ 60) + 'm left')
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://${endpoint.r}.tts.speech.microsoft.com/cognitiveservices/v1`;
|
||||||
|
const headers = {
|
||||||
|
'Authorization': endpoint.t,
|
||||||
|
'Content-Type': 'application/ssml+xml',
|
||||||
|
'User-Agent': 'okhttp/4.5.0',
|
||||||
|
'X-Microsoft-OutputFormat': outputFormat
|
||||||
|
};
|
||||||
|
const ssml = getSsml(text, voiceName, rate, pitch);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: ssml
|
||||||
|
});
|
||||||
|
if(response.ok) {
|
||||||
|
if (!download) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
resp = new Response(response.body,response)
|
||||||
|
resp.headers.set('Content-Disposition', `attachment; filename="${uuid()}.mp3"`);
|
||||||
|
return resp;
|
||||||
|
}else {
|
||||||
|
return new Response(response.statusText, { status: response.status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(unsafe) {
|
||||||
|
return unsafe.replace(/[<>&'"]/g, function (c) {
|
||||||
|
switch (c) {
|
||||||
|
case '<': return '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '&': return '&';
|
||||||
|
case '\'': return ''';
|
||||||
|
case '"': return '"';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSsml(text, voiceName, rate, pitch) {
|
||||||
|
text = escapeXml(text);
|
||||||
|
return `<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" version="1.0" xml:lang="zh-CN"> <voice name="${voiceName}"> <mstts:express-as style="general" styledegree="1.0" role="default"> <prosody rate="${rate}%" pitch="${pitch}%" volume="50">${text}</prosody> </mstts:express-as> </voice> </speak>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function voiceList() {
|
||||||
|
const headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26',
|
||||||
|
'X-Ms-Useragent': 'SpeechStudio/2021.05.001',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Origin': 'https://azure.microsoft.com',
|
||||||
|
'Referer': 'https://azure.microsoft.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch('https://eastus.api.speech.microsoft.com/cognitiveservices/voices/list', {
|
||||||
|
headers: headers,
|
||||||
|
cf: {
|
||||||
|
// Always cache this fetch regardless of content type
|
||||||
|
// for a max of 5 seconds before revalidating the resource
|
||||||
|
cacheTtl: 600,
|
||||||
|
cacheEverything: true,
|
||||||
|
cacheKey: "mstrans-voice-list",
|
||||||
|
},
|
||||||
|
}).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hmacSha256(key, data) {
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
||||||
|
false,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
const signature = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(data));
|
||||||
|
return new Uint8Array(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function base64ToBytes(base64) {
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bytesToBase64(bytes) {
|
||||||
|
const base64 = btoa(String.fromCharCode.apply(null, bytes));
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function uuid(){
|
||||||
|
return crypto.randomUUID().replace(/-/g, '')
|
||||||
|
}
|
||||||
5
workers/wrangler.toml
Normal file
5
workers/wrangler.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
name = "wstrans"
|
||||||
|
main = "src/index.js"
|
||||||
|
compatibility_date = "2024-04-15"
|
||||||
|
workers_dev = true
|
||||||
|
node_compat = true
|
||||||
Reference in New Issue
Block a user