feat: improve API key validation and enhance error handling in TTS service
This commit is contained in:
@@ -95,7 +95,7 @@ func (c *Client) getEndpoint(ctx context.Context) (map[string]interface{}, error
|
|||||||
// 更新缓存
|
// 更新缓存
|
||||||
c.endpointMu.Lock()
|
c.endpointMu.Lock()
|
||||||
c.endpoint = endpoint
|
c.endpoint = endpoint
|
||||||
c.endpointExpiry = time.Now().Add(30 * time.Minute) // 令牌有效期通常是1小时,提前刷新
|
c.endpointExpiry = time.Now().Add(5 * time.Minute)
|
||||||
c.endpointMu.Unlock()
|
c.endpointMu.Unlock()
|
||||||
|
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
let expiredAt = null;
|
let expiredAt = null;
|
||||||
let endpoint = null;
|
let endpoint = null;
|
||||||
const API_KEY = 'your-secret-api-key'; // 添加 API 密钥常量,可以修改为你想要的值
|
|
||||||
|
|
||||||
// 定义需要保留的 SSML 标签模式
|
// 定义需要保留的 SSML 标签模式
|
||||||
const preserveTags = [
|
const preserveTags = [
|
||||||
@@ -73,7 +72,15 @@ async function handleRequest(request) {
|
|||||||
|
|
||||||
// 验证 API 密钥
|
// 验证 API 密钥
|
||||||
if (!validateApiKey(apiKey)) {
|
if (!validateApiKey(apiKey)) {
|
||||||
return new Response('Unauthorized: Invalid API key', { status: 401 });
|
// 改进 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 text = requestUrl.searchParams.get('t') || '';
|
||||||
@@ -160,7 +167,7 @@ async function handleRequest(request) {
|
|||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 text-gray-800">
|
<body class="bg-gray-50 text-gray-800">
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
<nav class="bg-ms-blue shadow-lg">
|
<nav class="bg-ms-blue shadow-2xl">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex justify-between h-16">
|
<div class="flex justify-between h-16">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -174,146 +181,148 @@ async function handleRequest(request) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 主内容 -->
|
<!-- 主内容 -->
|
||||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
<!-- 主要功能区 -->
|
<div class="border-b border-gray-200">
|
||||||
<div class="flex flex-col lg:flex-row gap-8">
|
<nav class="-mb-px flex space-x-8 justify-center" aria-label="Tabs">
|
||||||
<!-- 左边栏:语音转换 -->
|
<button class="tab-link py-4 px-1 text-ms-blue border-b-2 border-ms-blue hover:text-ms-dark-blue" data-tab="tab1">在线文本转语音</button>
|
||||||
<div class="lg:w-2/3">
|
<button class="tab-link py-4 px-1 text-gray-500 hover:text-ms-dark-blue" data-tab="tab2">API文档</button>
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
|
<button class="tab-link py-4 px-1 text-gray-500 hover:text-ms-dark-blue" data-tab="tab3">关于服务</button>
|
||||||
<div class="px-4 py-5 sm:px-6">
|
</nav>
|
||||||
<h2 class="text-lg font-medium text-gray-900">在线文本转语音</h2>
|
</div>
|
||||||
<p class="mt-1 text-sm text-gray-500">输入文本并选择语音进行转换</p>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<form id="ttsForm" class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label for="apiKey" class="block text-sm font-medium text-gray-700">API Key</label>
|
|
||||||
<input type="text" id="apiKey" name="apiKey" required
|
|
||||||
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
|
|
||||||
placeholder="输入API Key" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div id="tab1" class="tab-content mt-6">
|
||||||
<label for="text" class="block text-sm font-medium text-gray-700">输入文本</label>
|
<!-- 在线文本转语音表单内容 -->
|
||||||
<textarea id="text" name="text" rows="4" required
|
<div class="flex flex-col lg:flex-row gap-8">
|
||||||
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
|
<!-- 左边栏:语音转换 -->
|
||||||
placeholder="请输入要转换的文本"></textarea>
|
<div class="lg:w-3/4 mx-auto">
|
||||||
</div>
|
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
|
||||||
|
<div class="px-4 py-5 sm:px-6">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<h2 class="text-lg font-medium text-gray-900">在线文本转语音</h2>
|
||||||
<div>
|
<p class="mt-1 text-sm text-gray-500">输入文本并选择语音进行转换</p>
|
||||||
<div class="flex justify-between mb-1">
|
</div>
|
||||||
<label for="languageFilter" class="block text-sm font-medium text-gray-700">语言</label>
|
<div class="px-4 py-5 sm:p-6">
|
||||||
</div>
|
<form id="ttsForm" class="space-y-6">
|
||||||
<select id="languageFilter" name="languageFilter"
|
<!-- 添加错误提示区域 -->
|
||||||
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm"
|
<div id="apiErrorAlert" class="rounded-md bg-red-50 p-4" style="display: none;">
|
||||||
onchange="filterVoicesByLanguage()">
|
<div class="flex">
|
||||||
<option value="zh">中文 (Chinese)</option>
|
<div class="flex-shrink-0">
|
||||||
<option value="all">所有语言</option>
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<option value="en">英文 (English)</option>
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 000 16zM8.707 7.293a1 1 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 101.414 1.414L10 11.414l1.293-1.293a1 1 001.414-1.414L11.414 10l1.293-1.293a1 1 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
<option value="ja">日文 (Japanese)</option>
|
</svg>
|
||||||
<option value="ko">韩文 (Korean)</option>
|
</div>
|
||||||
<option value="fr">法语 (French)</option>
|
<div class="ml-3">
|
||||||
<option value="de">德语 (German)</option>
|
<h3 class="text-sm font-medium text-red-800" id="apiErrorTitle">错误</h3>
|
||||||
<option value="es">西班牙语 (Spanish)</option>
|
<div class="mt-2 text-sm text-red-700">
|
||||||
<option value="ru">俄语 (Russian)</option>
|
<p id="apiErrorMessage"></p>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-between mb-1">
|
|
||||||
<label for="voice" class="block text-sm font-medium text-gray-700">选择语音</label>
|
|
||||||
</div>
|
|
||||||
<select id="voice" name="voice"
|
|
||||||
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label for="rate" class="block text-sm font-medium text-gray-700">语速调整</label>
|
|
||||||
<div class="flex items-center mt-2">
|
|
||||||
<span id="rateValue" class="w-8 text-sm text-gray-500">0</span>
|
|
||||||
<input type="range" id="rate" name="rate" min="-100" max="100" value="0"
|
|
||||||
class="mt-1 block w-full" oninput="document.getElementById('rateValue').textContent=this.value" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="pitch" class="block text-sm font-medium text-gray-700">音调调整</label>
|
|
||||||
<div class="flex items-center mt-2">
|
|
||||||
<span id="pitchValue" class="w-8 text-sm text-gray-500">0</span>
|
|
||||||
<input type="range" id="pitch" name="pitch" min="-100" max="100" value="0"
|
|
||||||
class="mt-1 block w-full" oninput="document.getElementById('pitchValue').textContent=this.value" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3">
|
|
||||||
<button type="submit"
|
|
||||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-ms-blue hover:bg-ms-dark-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ms-blue">
|
|
||||||
生成语音
|
|
||||||
</button>
|
|
||||||
<button type="button" id="downloadBtn" style="display:none;"
|
|
||||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
|
||||||
下载音频
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="voiceLoadError" role="alert" class="mt-4 rounded-md bg-red-50 p-4" style="display: none;">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293-1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<h3 class="text-sm font-medium text-red-800">无法加载语音列表</h3>
|
|
||||||
<div class="mt-2 text-sm text-red-700">
|
|
||||||
<p>显示默认语音列表。请检查网络连接或稍后再试。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="audioContainer" class="mt-6 rounded-md bg-gray-50 p-4 border border-gray-200" style="display: none;">
|
<div>
|
||||||
<audio id="audioPlayer" controls class="w-full"></audio>
|
<label for="apiKey" class="block text-sm font-medium text-gray-700">API Key</label>
|
||||||
|
<input type="text" id="apiKey" name="apiKey" required
|
||||||
|
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
|
||||||
|
placeholder="输入API Key" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="text" class="block text-sm font-medium text-gray-700">输入文本</label>
|
||||||
|
<textarea id="text" name="text" rows="4" required
|
||||||
|
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
|
||||||
|
placeholder="请输入要转换的文本"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label for="languageFilter" class="block text-sm font-medium text-gray-700">语言</label>
|
||||||
|
</div>
|
||||||
|
<select id="languageFilter" name="languageFilter"
|
||||||
|
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm"
|
||||||
|
onchange="filterVoicesByLanguage()">
|
||||||
|
<option value="zh">中文 (Chinese)</option>
|
||||||
|
<option value="all">所有语言</option>
|
||||||
|
<option value="en">英文 (English)</option>
|
||||||
|
<option value="ja">日文 (Japanese)</option>
|
||||||
|
<option value="ko">韩文 (Korean)</option>
|
||||||
|
<option value="fr">法语 (French)</option>
|
||||||
|
<option value="de">德语 (German)</option>
|
||||||
|
<option value="es">西班牙语 (Spanish)</option>
|
||||||
|
<option value="ru">俄语 (Russian)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<label for="voice" class="block text-sm font-medium text-gray-700">选择语音</label>
|
||||||
|
</div>
|
||||||
|
<select id="voice" name="voice"
|
||||||
|
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="rate" class="block text-sm font-medium text-gray-700">语速调整</label>
|
||||||
|
<div class="flex items-center mt-2">
|
||||||
|
<span id="rateValue" class="w-8 text-sm text-gray-500">0</span>
|
||||||
|
<input type="range" id="rate" name="rate" min="-100" max="100" value="0"
|
||||||
|
class="mt-1 block w-full" oninput="document.getElementById('rateValue').textContent=this.value" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="pitch" class="block text-sm font-medium text-gray-700">音调调整</label>
|
||||||
|
<div class="flex items-center mt-2">
|
||||||
|
<span id="pitchValue" class="w-8 text-sm text-gray-500">0</span>
|
||||||
|
<input type="range" id="pitch" name="pitch" min="-100" max="100" value="0"
|
||||||
|
class="mt-1 block w-full" oninput="document.getElementById('pitchValue').textContent=this.value" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-ms-blue hover:bg-ms-dark-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ms-blue">
|
||||||
|
生成语音
|
||||||
|
</button>
|
||||||
|
<button type="button" id="downloadBtn" style="display:none;"
|
||||||
|
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||||
|
下载音频
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="voiceLoadError" role="alert" class="mt-4 rounded-md bg-red-50 p-4" style="display: none;">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 000 16zM8.707 7.293a1 1 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 101.414 1.414L10 11.414l1.293-1.293a1 1 001.414-1.414L11.414 10l1.293-1.293a1 1 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">无法加载语音列表</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700">
|
||||||
|
<p>显示默认语音列表。请检查网络连接或稍后再试。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="audioContainer" class="mt-6 rounded-md bg-gray-50 p-4 border border-gray-200" style="display: none;">
|
||||||
|
<audio id="audioPlayer" controls class="w-full"></audio>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 右边栏:API文档 -->
|
<div id="tab2" class="tab-content mt-6" style="display: none;">
|
||||||
<div class="lg:w-1/3 space-y-6">
|
<!-- API文档部分 -->
|
||||||
<!-- 关于卡片 -->
|
<div class="w-full lg:w-2/3 mx-auto space-y-6">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900">关于服务</h3>
|
|
||||||
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>
|
|
||||||
Microsoft TTS API 是一个高质量的文本转语音服务,支持多种语言和声音。
|
|
||||||
通过简单的 API 调用,可以将文本转换为自然流畅的语音。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 bg-blue-50 p-3 rounded-md">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
|
||||||
<p class="text-sm text-blue-700">
|
|
||||||
支持 SSML 标签和 OpenAI 兼容接口
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API 文档链接 -->
|
<!-- API 文档链接 -->
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
|
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
|
||||||
<div class="px-4 py-5 sm:px-6">
|
<div class="px-4 py-5 sm:px-6">
|
||||||
@@ -366,7 +375,7 @@ curl ${baseUrl}/v1/audio/speech \\
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg class="h-5 w-5 text-amber-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-amber-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 11-2 0 1 1 12 0zm-1-8a1 1 00-1 1v3a1 1 001 1h1a1 1 100-2v-3a1 1 00-1-1H9z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
@@ -384,6 +393,38 @@ curl ${baseUrl}/v1/audio/speech \\
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="tab3" class="tab-content mt-6" style="display: none;">
|
||||||
|
<!-- 关于服务部分 -->
|
||||||
|
<div class="w-full lg:w-2/3 mx-auto space-y-6">
|
||||||
|
<!-- 关于卡片 -->
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">关于服务</h3>
|
||||||
|
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
|
<p>
|
||||||
|
Microsoft TTS API 是一个高质量的文本转语音服务,支持多种语言和声音。
|
||||||
|
通过简单的 API 调用,可以将文本转换为自然流畅的语音。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 bg-blue-50 p-3 rounded-md">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0116 0zm-7-4a1 1 11-2 0 1 1 12 0zm-1-8a1 1 00-1 1v3a1 1 001 1h1a1 1 100-2v-3a1 1 00-1-1H9z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||||
|
<p class="text-sm text-blue-700">
|
||||||
|
支持 SSML 标签和 OpenAI 兼容接口
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
@@ -401,6 +442,9 @@ curl ${baseUrl}/v1/audio/speech \\
|
|||||||
document.getElementById('ttsForm').addEventListener('submit', async function(e) {
|
document.getElementById('ttsForm').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 隐藏先前的错误信息
|
||||||
|
document.getElementById('apiErrorAlert').style.display = 'none';
|
||||||
|
|
||||||
const apiKey = document.getElementById('apiKey').value;
|
const apiKey = document.getElementById('apiKey').value;
|
||||||
const text = encodeURIComponent(document.getElementById('text').value);
|
const text = encodeURIComponent(document.getElementById('text').value);
|
||||||
const voice = document.getElementById('voice').value;
|
const voice = document.getElementById('voice').value;
|
||||||
@@ -408,18 +452,36 @@ curl ${baseUrl}/v1/audio/speech \\
|
|||||||
const pitch = document.getElementById('pitch').value;
|
const pitch = document.getElementById('pitch').value;
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
alert('请输入要转换的文本');
|
showError('请输入要转换的文本', '文本内容不能为空');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
alert('请输入API Key');
|
showError('请输入API Key', 'API密钥不能为空');
|
||||||
return;
|
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}\`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// 处理错误响应
|
||||||
|
if (response.status === 401) {
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
showError('认证失败', errorData.message || '无效的API密钥,请确保您提供了正确的密钥');
|
||||||
|
} catch (e) {
|
||||||
|
showError('认证失败', '无效的API密钥,请确保您提供了正确的密钥');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
showError('请求失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const audioPlayer = document.getElementById('audioPlayer');
|
const audioPlayer = document.getElementById('audioPlayer');
|
||||||
audioPlayer.src = url;
|
audioPlayer.src = url;
|
||||||
audioPlayer.play();
|
audioPlayer.play();
|
||||||
@@ -432,10 +494,21 @@ curl ${baseUrl}/v1/audio/speech \\
|
|||||||
window.location.href = downloadUrl;
|
window.location.href = downloadUrl;
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('生成音频失败: ' + error.message);
|
showError('生成音频失败', error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 显示错误信息的函数
|
||||||
|
function showError(title, message) {
|
||||||
|
const errorAlert = document.getElementById('apiErrorAlert');
|
||||||
|
document.getElementById('apiErrorTitle').textContent = title;
|
||||||
|
document.getElementById('apiErrorMessage').textContent = message;
|
||||||
|
errorAlert.style.display = 'block';
|
||||||
|
|
||||||
|
// 滚动到错误信息
|
||||||
|
errorAlert.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
// 按语言筛选语音
|
// 按语言筛选语音
|
||||||
function filterVoicesByLanguage() {
|
function filterVoicesByLanguage() {
|
||||||
const languageFilter = document.getElementById('languageFilter').value;
|
const languageFilter = document.getElementById('languageFilter').value;
|
||||||
@@ -593,10 +666,31 @@ curl ${baseUrl}/v1/audio/speech \\
|
|||||||
|
|
||||||
// 页面加载完成后加载语音列表
|
// 页面加载完成后加载语音列表
|
||||||
window.onload = loadVoices;
|
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';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }});
|
`, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8'}});
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener('fetch', event => {
|
addEventListener('fetch', event => {
|
||||||
@@ -708,7 +802,9 @@ async function bytesToBase64(bytes) {
|
|||||||
|
|
||||||
// API 密钥验证函数
|
// API 密钥验证函数
|
||||||
function validateApiKey(apiKey) {
|
function validateApiKey(apiKey) {
|
||||||
return apiKey === API_KEY;
|
// 从环境变量获取 API 密钥并进行验证
|
||||||
|
const expectedApiKey = API_KEY || '';
|
||||||
|
return apiKey === expectedApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVoice(text, voiceName = 'zh-CN-XiaoxiaoMultilingualNeural', rate = 0, pitch = 0, outputFormat='audio-24khz-48kbitrate-mono-mp3', download=false) {
|
async function getVoice(text, voiceName = 'zh-CN-XiaoxiaoMultilingualNeural', rate = 0, pitch = 0, outputFormat='audio-24khz-48kbitrate-mono-mp3', download=false) {
|
||||||
|
|||||||
@@ -2,4 +2,8 @@ name = "wstrans"
|
|||||||
main = "src/index.js"
|
main = "src/index.js"
|
||||||
compatibility_date = "2024-04-15"
|
compatibility_date = "2024-04-15"
|
||||||
workers_dev = true
|
workers_dev = true
|
||||||
node_compat = true
|
compatibility_flags = [ "nodejs_compat" ]
|
||||||
|
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
API_KEY = "zuoban"
|
||||||
Reference in New Issue
Block a user