const encoder = new TextEncoder(); let expiredAt = null; let endpoint = null; // 添加缓存相关变量 let voiceListCache = null; let voiceListCacheTime = null; let voiceListPromise = null; const VOICE_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24小时,单位毫秒 // 定义需要保留的 SSML 标签模式 const preserveTags = [ { name: 'break', pattern: /]*\/>/g }, { name: 'speak', pattern: /|<\/speak>/g }, { name: 'prosody', pattern: /]*>|<\/prosody>/g }, { name: 'emphasis', pattern: /]*>|<\/emphasis>/g }, { name: 'voice', pattern: /]*>|<\/voice>/g }, { name: 'say-as', pattern: /]*>|<\/say-as>/g }, { name: 'phoneme', pattern: /]*>|<\/phoneme>/g }, { name: 'audio', pattern: /]*>|<\/audio>/g }, { name: 'p', pattern: /

|<\/p>/g }, { name: 's', pattern: /|<\/s>/g }, { name: 'sub', pattern: /]*>|<\/sub>/g }, { name: 'mstts', pattern: /]*>|<\/mstts:[^>]*>/g } ]; function uuid() { return crypto.randomUUID().replace(/-/g, '') } // EscapeSSML 转义 SSML 内容,但保留配置的标签 function escapeSSML(ssml) { // 使用占位符替换标签 let placeholders = new Map(); let processedSSML = ssml; let counter = 0; // 处理所有配置的标签 for (const tag of preserveTags) { processedSSML = processedSSML.replace(tag.pattern, function (match) { const placeholder = `__SSML_PLACEHOLDER_${tag.name}_${counter++}__`; placeholders.set(placeholder, match); return placeholder; }); } // 对处理后的文本进行HTML转义 let escapedContent = escapeBasicXml(processedSSML); // 恢复所有标签占位符 placeholders.forEach((tag, placeholder) => { escapedContent = escapedContent.replace(placeholder, tag); }); return escapedContent; } // 基本 XML 转义功能,只处理基本字符 function escapeBasicXml(unsafe) { return unsafe.replace(/[<>&'"]/g, function (c) { switch (c) { case '<': return '<'; case '>': return '>'; case '&': return '&'; case '\'': return '''; case '"': return '"'; } }); } async function handleRequest(request) { const requestUrl = new URL(request.url); const path = requestUrl.pathname; if (path === '/tts' || path === '/api/v1/tts') { // 从请求参数获取 API 密钥 const apiKey = requestUrl.searchParams.get('api_key'); // 验证 API 密钥 if (!validateApiKey(apiKey)) { // 改进 401 错误响应,提供更友好的错误信息 return new Response(JSON.stringify({ error: 'Unauthorized', message: '无效的 API 密钥,请确保您提供了正确的密钥。', status: 401 }), { status: 401, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } 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 style = requestUrl.searchParams.get('s') || 'general'; 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, style, outputFormat, download); return response; } // 添加 reader.json 路径处理 if (path === '/reader.json') { // 从请求参数获取 API 密钥 const apiKey = requestUrl.searchParams.get('api_key'); // 验证 API 密钥 if (!validateApiKey(apiKey)) { return new Response(JSON.stringify({ error: 'Unauthorized', message: '无效的 API 密钥,请确保您提供了正确的密钥。', status: 401 }), { status: 401, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } // 从URL参数获取 const voice = requestUrl.searchParams.get('v') || ''; const rate = requestUrl.searchParams.get('r') || ''; const pitch = requestUrl.searchParams.get('p') || ''; const style = requestUrl.searchParams.get('s') || ''; const displayName = requestUrl.searchParams.get('n') || 'Microsoft TTS'; // 构建基本URL const baseUrl = `${requestUrl.protocol}//${requestUrl.host}`; // 构建URL参数 const urlParams = ["t={{java.encodeURI(speakText)}}", "r={{speakSpeed*4}}"]; // 只有有值的参数才添加 if (voice) { urlParams.push(`v=${voice}`); } if (pitch) { urlParams.push(`p=${pitch}`); } if (style) { urlParams.push(`s=${style}`); } // 只有配置了API密钥且请求提供了api_key参数时才添加 if (API_KEY && apiKey) { urlParams.push(`api_key=${apiKey}`); } const url = `${baseUrl}/tts?${urlParams.join('&')}`; // 返回 reader 响应 return new Response(JSON.stringify({ id: Date.now(), name: displayName, url: url }), { status: 200, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } // 添加 ifreetime.json 路径处理 if (path === '/ifreetime.json') { // 从请求参数获取 API 密钥 const apiKey = requestUrl.searchParams.get('api_key'); // 验证 API 密钥 if (!validateApiKey(apiKey)) { return new Response(JSON.stringify({ error: 'Unauthorized', message: '无效的 API 密钥,请确保您提供了正确的密钥。', status: 401 }), { status: 401, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } // 从URL参数获取 const voice = requestUrl.searchParams.get('v') || ''; const rate = requestUrl.searchParams.get('r') || ''; const pitch = requestUrl.searchParams.get('p') || ''; const style = requestUrl.searchParams.get('s') || ''; const displayName = requestUrl.searchParams.get('n') || 'Microsoft TTS'; // 构建基本URL const baseUrl = `${requestUrl.protocol}//${requestUrl.host}`; const url = `${baseUrl}/tts`; // 生成随机的唯一ID const ttsConfigID = crypto.randomUUID(); // 构建请求参数 const params = { "t": "%@", // %@ 是 IFreeTime 中的文本占位符 "v": voice, "r": rate, "p": pitch, "s": style }; // 只有配置了API密钥且请求提供了api_key参数时才添加 if (API_KEY && apiKey) { params["api_key"] = apiKey; } // 构建响应 const response = { loginUrl: "", maxWordCount: "", customRules: {}, ttsConfigGroup: "Azure", _TTSName: displayName, _ClassName: "JxdAdvCustomTTS", _TTSConfigID: ttsConfigID, httpConfigs: { useCookies: 1, headers: {} }, voiceList: [], ttsHandles: [ { paramsEx: "", processType: 1, maxPageCount: 1, nextPageMethod: 1, method: 1, requestByWebView: 0, parser: {}, nextPageParams: {}, url: url, params: params, httpConfigs: { useCookies: 1, headers: {} } } ] }; // 返回 IFreeTime 响应 return new Response(JSON.stringify(response), { status: 200, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); } // 添加 OpenAI 兼容接口路由 if (path === '/v1/audio/speech' || path === '/audio/speech') { return await handleOpenAITTS(request); } if (path === '/voices' || path === '/api/v1/voices') { const cacheUrl = new URL(request.url); cacheUrl.searchParams.set('__cache_version', 'voices-v2'); const cacheRequest = new Request(cacheUrl.toString(), request); const cachedResponse = await caches.default.match(cacheRequest); if (cachedResponse) { console.log('使用边缘缓存的语音列表响应'); return cachedResponse; } const l = (requestUrl.searchParams.get('l') || '').toLowerCase(); let response = await voiceList(); if (l.length > 0) { response = response.filter(item => item.Locale.toLowerCase().includes(l)); } const voicesResponse = new Response(JSON.stringify(response), { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=86400' } }); await caches.default.put(cacheRequest, voicesResponse.clone()); return voicesResponse; } const baseUrl = request.url.split('://')[0] + "://" + requestUrl.host; return new Response(` Microsoft TTS API

在线文本转语音

输入文本并选择语音进行转换

0
0

© ${new Date().getFullYear()} TTS

`, { 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': generateUserId(), // 使用随机生成的UserId 'X-HomeGeographicRegion': 'zh-Hans-CN', 'X-ClientTraceId': uuid(), // 直接使用uuid函数生成 'X-MT-Signature': await sign(endpointUrl), 'User-Agent': 'okhttp/4.5.0', 'Content-Type': 'application/json', 'Content-Length': '0', 'Accept-Encoding': 'gzip' }; return fetch(endpointUrl, { method: 'POST', headers: headers }).then(res => res.json()); } // 随机生成 X-UserId,格式为 16 位字符(字母+数字) function generateUserId() { const chars = 'abcdef0123456789'; // 只使用16进制字符,与原格式一致 let result = ''; for (let i = 0; i < 16; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } 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(); } function getSsml(text, voiceName, rate, pitch, style = 'general') { text = escapeSSML(text); return ` ${text} `; } function voiceList() { // 检查缓存是否有效 if (voiceListCache && voiceListCacheTime && (Date.now() - voiceListCacheTime) < VOICE_CACHE_DURATION) { console.log('使用缓存的语音列表数据,剩余有效时间:', Math.round((VOICE_CACHE_DURATION - (Date.now() - voiceListCacheTime)) / 60000), '分钟'); return Promise.resolve(voiceListCache); } if (voiceListPromise) { console.log('复用正在获取的语音列表请求'); return voiceListPromise; } console.log('获取新的语音列表数据'); 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' }; voiceListPromise = fetch('https://eastus.api.speech.microsoft.com/cognitiveservices/voices/list', { headers: headers, }) .then(async res => { if (!res.ok) { throw new Error(`获取语音列表失败: ${res.status} ${res.statusText}`); } return res.json(); }) .then(data => { if (!Array.isArray(data)) { throw new Error('获取语音列表失败: 响应格式异常'); } // 更新缓存 voiceListCache = data; voiceListCacheTime = Date.now(); return data; }) .catch(error => { if (voiceListCache) { console.error('刷新语音列表失败,继续使用旧缓存:', error); return voiceListCache; } throw error; }) .finally(() => { voiceListPromise = null; }); return voiceListPromise; } 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; } // API 密钥验证函数 function validateApiKey(apiKey) { // 从环境变量获取 API 密钥并进行验证 const expectedApiKey = API_KEY || ''; return apiKey === expectedApiKey; } async function getVoice(text, voiceName = 'zh-CN-XiaoxiaoMultilingualNeural', rate = 0, pitch = 0, style = 'general', outputFormat = 'audio-24khz-48kbitrate-mono-mp3', download = false) { // get expiredAt from endpoint.t (jwt token) if (!expiredAt || Date.now() / 1000 > expiredAt - 300) { 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, style); const response = await fetch(url, { method: 'POST', headers: headers, body: ssml }); if (response.ok) { if (!download) { return response; } const resp = new Response(response.body, response); resp.headers.set('Content-Disposition', `attachment; filename="${uuid()}.mp3"`); return resp; } else { const errorText = await response.text(); console.error('TTS API错误:', response.status, errorText); if (response.status === 400 || response.status === 401 || response.status === 403) { 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 retry, expiredAt:' + (seconds / 60) + 'm left'); headers['Authorization'] = endpoint.t; const retryUrl = `https://${endpoint.r}.tts.speech.microsoft.com/cognitiveservices/v1`; const retryResponse = await fetch(retryUrl, { method: 'POST', headers: headers, body: ssml }); if (retryResponse.ok) { if (!download) { return retryResponse; } const retryResp = new Response(retryResponse.body, retryResponse); retryResp.headers.set('Content-Disposition', `attachment; filename="${uuid()}.mp3"`); return retryResp; } const retryText = await retryResponse.text(); console.error('TTS API重试失败:', retryResponse.status, retryText); return new Response(retryText || retryResponse.statusText, { status: retryResponse.status }); } return new Response(errorText || response.statusText, { status: response.status }); } } // 处理 OpenAI 格式的文本转语音请求 async function handleOpenAITTS(request) { // 验证请求方法是否为 POST if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } // 验证 API 密钥 const authHeader = request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return new Response(JSON.stringify({ error: 'Unauthorized: Missing or invalid API key' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } const apiKey = authHeader.replace('Bearer ', ''); if (!validateApiKey(apiKey)) { return new Response(JSON.stringify({ error: 'Unauthorized: Invalid API key' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } try { // 解析请求体 JSON const requestData = await request.json(); // 验证必要参数 if (!requestData.model || !requestData.input) { return new Response(JSON.stringify({ error: 'Bad request: Missing required parameters' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 提取参数 const text = requestData.input; // 映射 voice 参数 (可选择添加 model 到 voice 的映射逻辑) let voiceName = 'zh-CN-XiaoxiaoMultilingualNeural'; // 默认声音 if (requestData.voice) { // OpenAI的voice参数有alloy, echo, fable, onyx, nova, shimmer // 可以根据需要进行映射 const voiceMap = { 'alloy': 'zh-CN-XiaoxiaoMultilingualNeural', 'echo': 'zh-CN-YunxiNeural', 'fable': 'zh-CN-XiaomoNeural', 'onyx': 'zh-CN-YunjianNeural', 'nova': 'zh-CN-XiaochenNeural', 'shimmer': 'en-US-AriaNeural' }; voiceName = voiceMap[requestData.voice] || requestData.voice; } // 速度和音调映射 (OpenAI 使用 0.25-4.0,我们使用 -100 到 100) let rate = 0; if (requestData.speed) { // 映射 0.25-4.0 到 -100 到 100 范围 // 1.0 是正常速度,对应 rate=0 rate = Math.round((requestData.speed - 1.0) * 100); // 限制范围 rate = Math.max(-100, Math.min(100, rate)); } // 设置输出格式 const outputFormat = requestData.response_format === 'opus' ? 'audio-48khz-192kbitrate-mono-opus' : 'audio-24khz-48kbitrate-mono-mp3'; // 调用 TTS API const ttsResponse = await getVoice(text, voiceName, rate, 0, requestData.model, outputFormat, false); return ttsResponse; } catch (error) { console.error('OpenAI TTS API error:', error); return new Response(JSON.stringify({ error: 'Internal server error: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }