diff --git a/workers/src/index.js b/workers/src/index.js index 8af3a86..8e1557d 100644 --- a/workers/src/index.js +++ b/workers/src/index.js @@ -1,6 +1,10 @@ const encoder = new TextEncoder(); let expiredAt = null; let endpoint = null; +// 添加缓存相关变量 +let voiceListCache = null; +let voiceListCacheTime = null; +const VOICE_CACHE_DURATION =4 *60 * 60 * 1000; // 4小时,单位毫秒 // 定义需要保留的 SSML 标签模式 const preserveTags = [ @@ -200,11 +204,55 @@ async function handleRequest(request) { placeholder="请输入要转换的文本"> -
- - +
+
+ + +
+ +
+ + +
@@ -274,13 +322,14 @@ async function handleRequest(request) {

文本转语音 API

- /tts?api_key={key}&t={text}&v={voice}&r={rate}&p={pitch} + /tts?api_key={key}&t={text}&v={voice}&r={rate}&p={pitch}&s={style}

OpenAI 兼容接口

@@ -382,6 +431,33 @@ curl ${baseUrl}/v1/audio/speech \\ // 存储所有语音数据 let allVoices = []; + // 在表单提交事件监听器之前添加语音选择变更事件监听 + document.addEventListener('DOMContentLoaded', function() { + // 添加对语音选择变化的监听 + document.getElementById('voice').addEventListener('change', function() { + updateStyleOptions(this.value); + }); + + const tabLinks = document.querySelectorAll('.tab-link'); + const tabContents = document.querySelectorAll('.tab-content'); + + tabLinks.forEach(link => { + link.addEventListener('click', () => { + tabLinks.forEach(l => { + l.classList.remove('text-ms-blue', 'border-ms-blue'); + l.classList.add('text-gray-500'); + }); + link.classList.add('text-ms-blue', 'border-ms-blue'); + link.classList.remove('text-gray-500'); + + const targetId = link.getAttribute('data-tab'); + tabContents.forEach(tc => { + tc.style.display = (tc.id === targetId) ? '' : 'none'; + }); + }); + }); + }); + document.getElementById('ttsForm').addEventListener('submit', async function(e) { e.preventDefault(); @@ -393,6 +469,7 @@ curl ${baseUrl}/v1/audio/speech \\ const voice = document.getElementById('voice').value; const rate = document.getElementById('rate').value; const pitch = document.getElementById('pitch').value; + const style = document.getElementById('style').value; // 获取选择的风格 if (!text) { showError('请输入要转换的文本', '文本内容不能为空'); @@ -404,7 +481,7 @@ curl ${baseUrl}/v1/audio/speech \\ return; } - const url = \`${baseUrl}/tts?api_key=\${apiKey}&t=\${text}&v=\${voice}&r=\${rate}&p=\${pitch}\`; + const url = \`${baseUrl}/tts?api_key=\${apiKey}&t=\${text}&v=\${voice}&r=\${rate}&p=\${pitch}&s=\${style}\`; try { const response = await fetch(url); @@ -452,6 +529,69 @@ curl ${baseUrl}/v1/audio/speech \\ errorAlert.scrollIntoView({ behavior: 'smooth', block: 'center' }); } + // 更新风格选项的函数 + function updateStyleOptions(voiceName) { + const styleSelect = document.getElementById('style'); + // 清空现有选项 + styleSelect.innerHTML = ''; + + // 默认添加标准风格 + const defaultOption = document.createElement('option'); + defaultOption.value = 'general'; + defaultOption.text = '标准'; + styleSelect.appendChild(defaultOption); + + // 查找选定的语音对象 + const selectedVoice = allVoices.find(v => v.ShortName === voiceName); + + if (selectedVoice && selectedVoice.StyleList && selectedVoice.StyleList.length > 0) { + // 对风格列表进行排序 + const styles = [...selectedVoice.StyleList].sort(); + + // 为每个风格创建选项 + styles.forEach(style => { + // 跳过已经添加的"general" + if (style.toLowerCase() === 'general') return; + + const option = document.createElement('option'); + option.value = style; + // 根据风格名称进行本地化显示 + option.text = getStyleDisplayName(style); + styleSelect.appendChild(option); + }); + } + } + + // 风格名称本地化显示 + function getStyleDisplayName(styleName) { + const styleMap = { + 'angry': '愤怒', + 'cheerful': '欢快', + 'sad': '悲伤', + 'fearful': '恐惧', + 'disgruntled': '不满', + 'serious': '严肃', + 'affectionate': '深情', + 'gentle': '温柔', + 'embarrassed': '尴尬', + 'assistant': '助手', + 'calm': '平静', + 'chat': '聊天', + 'excited': '兴奋', + 'friendly': '友好', + 'hopeful': '希望', + 'narration-professional': '专业叙述', + 'newscast': '新闻播报', + 'newscast-casual': '随性新闻', + 'poetry-reading': '诗歌朗诵', + 'shouting': '喊叫', + 'sports-commentary': '体育解说', + 'whispering': '低语' + }; + + return styleMap[styleName.toLowerCase()] || styleName; + } + // 加载可用语音列表 async function loadVoices() { try { @@ -500,6 +640,9 @@ curl ${baseUrl}/v1/audio/speech \\ if (voiceSelect.querySelector(\`option[value="\${defaultVoice}"]\`)) { voiceSelect.value = defaultVoice; } + + // 加载初始选择语音的风格选项 + updateStyleOptions(voiceSelect.value); } else { console.error('获取语音列表失败:', response.status); showDefaultVoices(); @@ -563,31 +706,19 @@ curl ${baseUrl}/v1/audio/speech \\ // 默认选择晓晓多语言 voiceSelect.value = "zh-CN-XiaoxiaoMultilingualNeural"; + + // 设置默认风格 + const styleSelect = document.getElementById('style'); + styleSelect.innerHTML = ''; + const defaultOption = document.createElement('option'); + defaultOption.value = 'general'; + defaultOption.text = '标准'; + styleSelect.appendChild(defaultOption); } // 页面加载完成后加载语音列表 window.onload = loadVoices; - document.addEventListener('DOMContentLoaded', function() { - const tabLinks = document.querySelectorAll('.tab-link'); - const tabContents = document.querySelectorAll('.tab-content'); - - tabLinks.forEach(link => { - link.addEventListener('click', () => { - tabLinks.forEach(l => { - l.classList.remove('text-ms-blue', 'border-ms-blue'); - l.classList.add('text-gray-500'); - }); - link.classList.add('text-ms-blue', 'border-ms-blue'); - link.classList.remove('text-gray-500'); - - const targetId = link.getAttribute('data-tab'); - tabContents.forEach(tc => { - tc.style.display = (tc.id === targetId) ? '' : 'none'; - }); - }); - }); - }); @@ -598,6 +729,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 = { @@ -653,6 +786,14 @@ function getSsml(text, voiceName, rate, pitch, style = 'general') { } 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); + } + + 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', @@ -663,14 +804,14 @@ function voiceList() { 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()); + }) + .then(res => res.json()) + .then(data => { + // 更新缓存 + voiceListCache = data; + voiceListCacheTime = Date.now(); + return data; + }); } async function hmacSha256(key, data) {