はじめに
エッジTTSワーカー(依存関係) エッジツ Edge TTS WorkerはCloudflare Worker上にデプロイされたプロキシサービスで、Microsoft Edge TTSサービスをOpenAIフォーマットと互換性のあるAPIインターフェースにカプセル化します。Edge TTS Workerは、中国語、英語、日本語、韓国語などの多言語をサポートし、Cloudflare Worker Free Planに基づく完全無料です。また、このサービスはカスタムAPIキーにも対応しており、セキュリティと制御を確実にし、数分以内の迅速な導入が可能です。
テストAPI: https://tts.aishare.us.kg/ KEY: aisharenet
API用のシンプルなインターフェースをパッケージ化するプロジェクト
APIとインターフェイスをCloudflareのデプロイメントとともにパッケージ化:
完全なコードはCHATGPTによって生成され、そのコードは記事の最後に添付されている。 AIプログラミング ツールまたは使用 トリクル (オンライン・アクセシブル・スピーチ生成サービスのワンクリック生成):
機能一覧
- OpenAI互換のインターフェース形式を提供
- メインランドのアクセス制限を回避し、マイクロソフト・サービスの認証ステップをなくす
- 中国語、英語、日本語、韓国語など多言語対応。
- Cloudflare Worker Free Planに基づく完全無料。
- カスタムAPIキーのサポートにより、セキュリティとコントロールを確保
- 迅速な展開、数分で準備完了
- さまざまな音声効果をテストするためのテストスクリプトを提供する。
ヘルプの使用
設置プロセス
- ワーカーの作成
- Cloudflare Dashboardにログインする
- Workers & Pagesに移動し、Create Workerをクリックします。
- ワーカーに名前をつける(例:edge-ts)
- 配備コード
- エディターからデフォルトのコードを削除する
- のコピーを取る。
ワーカー
にコードを貼り付けます。 - 保存してデプロイをクリック
- APIキーの設定(オプション)
- ワーカーの設定ページでSettings -> Variablesを見つける。
- Add variableをクリックし、API_KEYに名前とキーの値を入力する。
- 保存してデプロイをクリック
- カスタムドメイン名の設定(オプション)
- 前提条件:ドメインがすでにCloudflareでホストされており、ドメインのDNSレコードがCloudflare経由でプロキシされている(プロキシのステータスがオレンジクラウドである)
- 設定ステップ:
- ワーカーの詳細ページのSettingsタブをクリックする。
- Domain and Routingセクションを探し、Addボタンをクリックする。
- Custom Domainを選択し、使用するドメイン名(例:tts.example.com)を入力します。
- Add Domainをクリックし、証明書の展開が完了するまで待つ(通常は数分以内)。
使用方法
- TTY(音声合成インターフェース)
- 中国語のスピーチ例
curl -X POST https://你的worker地址/v1/audio/speech -H "Content-Type: application/json" ¦ -H "Authorization: Bearer your-api-key -H "Authorization: Bearer your-api-key" ୧-͈ᴗ-͈ -d '{ "model": "tts-1", "input": "Hello". 「入力": "Hello, world!, 「音声": "zh-CN-XiaoxiaoNeural"、 "speed": 1.0, "response_format". "ピッチ": 1.0, "スタイル": "一般" }' --出力 chinese.mp3
- 英語のスピーチの例:
curl -X POST https://你的worker地址/v1/audio/speech -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" ୧-͈ᴗ-͈ -d '{ "model": "tts-1", "input": "Hello World". "input": "Hello, World!", "voice": "en-US". 「voice": "en-US-JennyNeural"、 "pitch": 1.0, "style": "general". "style": "general". }' --出力 english.mp3
- テストスクリプトの使用
- テストスクリプトのダウンロード
test_voices.sh
- スクリプトに実行権限を追加する:
バッシュ
chmod +x test_voices.sh
- スクリプトを実行する:
バッシュ
./test_voices.sh [APIキー]。 - 例
バッシュ
# APIキーの使用
./test_voices.sh https://your-worker.workers.dev your-api-key
#はAPIキーを使用しません。
./test_voices.sh https://your-worker.workers.dev
- スクリプトは、サポートされている各音声のテスト音声ファイルを生成し、それを再生して最適な音声を選択することができます。
- テストスクリプトのダウンロード
APIパラメータ 説明
モデル
(文字列): モデル名(固定値)。ts-1
入力
(文字列):変換するテキスト。"ハロー、ワールド!"
声
(文字列): 声の名前。zh-CN-XiaoxiaoNeural
レスポンス・フォーマット
(文字列、 オプ シ ョ ナル) : 出力形式。mp3
スピード
(数値、オプション): 発話速度 (0.5-2.0)、デフォルトは1.0
ピッチ
(数値、オプション): 音色 (0.5-2.0)、デフォルトは1.0
スタイル
(string, optional): 感情。一般
対応音声一覧
音声に対応する言語のテキストを必ず使用してください。例えば、中国語の音声には中国語のテキストを使用してください。以下はよく使われる音声の例です:
zh-CN-XiaoxiaoNeural
シャオシャオ - 温かく生き生きとzh-CN-XiaoyiNeural
シャオイー - 温かさと優しさzh-CN-YunxiNeural
:: Yumshi - 男声、安定した声zh-中国-雲陽ニューラル
:: ユン・ヤン - 男声、プロフェッショナルzh-CN-XiaohanNeural
シャオハン - ナチュラル・フローzh-CN-XiaomengNeural
シャオメン - 甘くて元気zh-CN-XiaochenNeural
シャオチェン - 穏やかでやさしい- 待って...
workers.js コード
const API_KEY = 'aisharenet'; // 実際のAPIキーに置き換える const TOKEN_REFRESH_BEFORE_EXPIRY = 3 * 60; // 実際のAPIキーに置き換える。 トークン 刷新时间(秒) const DEFAULT_VOICE = 'zh-CN-XiaoxiaoNeural'; // 默认语音 const DEFAULT_SPEED = 1.0; // 默认语速 const DEFAULT_PITCH = 1.0; // 默认音调 let tokenInfo = { endpoint: null, token: null, expiredAt: null }; // 处理请求 addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { const url = new URL(request.url); // 如果请求根路径,返回 Web UI if (url.pathname === '/') { return new Response(getWebUI(), { headers: { 'Content-Type': 'text/html' }, }); } // 处理 TTS 请求 if (url.pathname === '/v1/audio/speech') { return handleTTSRequest(request); } // 默认返回 404 return new Response('Not Found', { status: 404 }); } // 处理 TTS 请求 async function handleTTSRequest(request) { if (request.method === 'OPTIONS') { return handleOptions(request); } // 验证 API Key const authHeader = request.headers.get('authorization') || request.headers.get('x-api-key'); const apiKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null; if (API_KEY && apiKey !== API_KEY) { return createErrorResponse('Invalid API key. Use \'Authorization: Bearer your-api-key\' header', 'invalid_api_key', 401); } try { const requestBody = await request.json(); const { model = "tts-1", input, voice = DEFAULT_VOICE, speed = DEFAULT_SPEED, pitch = DEFAULT_PITCH } = requestBody; // 验证参数范围 validateParameterRange('speed', speed, 0.5, 2.0); validateParameterRange('pitch', pitch, 0.5, 2.0); const rate = calculateRate(speed); const numPitch = calculatePitch(pitch); const audioResponse = await getVoice( input, voice, rate >= 0 ? `+${rate}%` : `${rate}%`, numPitch >= 0 ? `+${numPitch}%` : `${numPitch}%`, '+0%', 'general', // 固定风格为通用 'audio-24khz-48kbitrate-mono-mp3' // 固定音频格式为 MP3 ); return audioResponse; } catch (error) { return createErrorResponse(error.message, 'edge_tts_error', 500); } } // 处理 OPTIONS 请求 function handleOptions(request) { return new Response(null, { headers: { ...makeCORSHeaders(), 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', 'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers') || 'Authorization', }, }); } // 创建错误响应 function createErrorResponse(message, code, status) { return new Response( JSON.stringify({ error: { message, code } }), { status, headers: { 'Content-Type': 'application/json', ...makeCORSHeaders() }, } ); } // 验证参数范围 function validateParameterRange(name, value, min, max) { if (value < min || value > max) { throw new Error(`${name} must be between ${min} and ${max}`); } } // 计算语速 function calculateRate(speed) { return parseInt(String((parseFloat(speed) - 1.0) * 100)); } // 计算音调 function calculatePitch(pitch) { return parseInt(String((parseFloat(pitch) - 1.0) * 100)); } // 生成 CORS 头 function makeCORSHeaders() { return { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, x-api-key', 'Access-Control-Max-Age': '86400', }; } // 获取语音 async function getVoice(text, voiceName, rate, pitch, volume, style, outputFormat) { try { const chunks = text.trim().split("\n"); const audioChunks = await Promise.all(chunks.map(chunk => getAudioChunk(chunk, voiceName, rate, pitch, volume, style, outputFormat))); // 将音频片段拼接起来 const concatenatedAudio = new Blob(audioChunks, { type: `audio/${outputFormat.split('-').pop()}` }); return new Response(concatenatedAudio, { headers: { 'Content-Type': `audio/${outputFormat.split('-').pop()}`, ...makeCORSHeaders(), }, }); } catch (error) { console.error("语音合成失败:", error); return createErrorResponse(error.message, 'edge_tts_error', 500); } } // 获取单个音频片段 async function getAudioChunk(text, voiceName, rate, pitch, volume, style, outputFormat) { const endpoint = await getEndpoint(); const url = `https://${endpoint.r}.tts.speech.microsoft.com/cognitiveservices/v1`; const slien = extractSilenceDuration(text); const response = await fetch(url, { method: "POST", headers: { "Authorization": endpoint.t, "Content-Type": "application/ssml+xml", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0", "X-Microsoft-OutputFormat": outputFormat, }, body: getSsml(text, voiceName, rate, pitch, volume, style, slien), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Edge TTS API error: ${response.status} ${errorText}`); } return response.blob(); } // 提取静音时长 function extractSilenceDuration(text) { const match = text.match(/\[(\d+)\]\s*?$/); return match && match.length === 2 ? parseInt(match[1]) : 0; } // 生成 SSML function getSsml(text, voiceName, rate, pitch, volume, style, slien) { const slienStr = slien > 0 ? `` : ''; return ` ${text} ${slienStr} `; } // 获取 Endpoint async function getEndpoint() { const now = Date.now() / 1000; if (tokenInfo.token && tokenInfo.expiredAt && now < tokenInfo.expiredAt - TOKEN_REFRESH_BEFORE_EXPIRY) { return tokenInfo.endpoint; } // 获取新 Token const endpointUrl = "https://dev.microsofttranslator.com/apps/endpoint?api-version=1.0"; const clientId = crypto.randomUUID().replace(/-/g, ""); try { const response = await fetch(endpointUrl, { method: "POST", 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": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0", "Content-Type": "application/json; charset=utf-8", "Content-Length": "0", "Accept-Encoding": "gzip", }, }); if (!response.ok) { throw new Error(`获取 Endpoint 失败: ${response.status}`); } const data = await response.json(); const jwt = data.t.split(".")[1]; const decodedJwt = JSON.parse(atob(jwt)); tokenInfo = { endpoint: data, token: data.t, expiredAt: decodedJwt.exp, }; return data; } catch (error) { console.error("获取 Endpoint 失败:", error); if (tokenInfo.token) { console.log("使用过期的缓存 Token"); return tokenInfo.endpoint; } throw error; } } // 签名 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() { return (new Date()).toUTCString().replace(/GMT/, "").trim() + " GMT"; } // HMAC SHA-256 签名 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); } // Base64 转字节数组 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; } // 字节数组转 Base64 async function bytesToBase64(bytes) { return btoa(String.fromCharCode.apply(null, bytes)); } // 生成 UUID function uuid() { return crypto.randomUUID().replace(/-/g, ""); } // 获取 Web UI function getWebUI() { return `
音声合成ツール
音声を生成しています。
`; }