Edge TTS Worker : Déploiement des API de synthèse vocale de Microsoft à l'aide de Cloudflare, d'un format compatible avec OpenAI et d'une interface Web packagée

Introduction générale

Edge TTS Worker (dépendances) edge-tts Edge TTS Worker est un service proxy déployé sur Cloudflare Worker, qui encapsule le service Microsoft Edge TTS dans une interface API compatible avec le format OpenAI. Avec ce projet, les utilisateurs peuvent facilement utiliser le service de synthèse vocale de haute qualité de Microsoft sans la certification de Microsoft.Edge TTS Worker fournit un support multilingue, y compris le chinois, l'anglais, le japonais, le coréen, etc. et est entièrement gratuit, basé sur le plan gratuit de Cloudflare Worker. Le service prend également en charge les clés API personnalisées pour garantir la sécurité et le contrôle, et peut être déployé rapidement, en quelques minutes.

Test API : https://tts.aishare.us.kg/ KEY : aisharenet

Un projet visant à créer une interface simple pour l'API

Edge TTS Worker:使用Cloudflare部署微软语音合成API,兼容OpenAI 格式并封装Web界面

 

Emballage des API et des interfaces avec déploiement de Cloudflare :

Le code complet est généré par CHATGPT, et le code est joint à la fin de l'article, avec la possibilité d'en ajouter d'autres. Programmation de l'IA Outils ou utilisation Trickle (Génération en un clic de services de génération de discours accessibles en ligne) :

Edge TTS Worker:使用Cloudflare部署微软语音合成API,兼容OpenAI 格式并封装Web界面

Expérience : https://edgetts.aishare.us.kg/

 

Liste des fonctions

  • Fournit des formats d'interface compatibles avec l'OpenAI
  • Contourner les restrictions d'accès au continent et éliminer l'étape d'authentification des services Microsoft
  • Prise en charge de plusieurs langues, dont le chinois, l'anglais, le japonais, le coréen, etc.
  • Totalement gratuit, basé sur le plan Cloudflare Worker Free
  • Prise en charge des clés d'API personnalisées pour garantir la sécurité et le contrôle
  • Déploiement rapide, prêt en quelques minutes
  • Fournir des scripts de test pour tester différents effets vocaux

 

Utiliser l'aide

Processus d'installation

  1. Création d'un travailleur
    • Se connecter au tableau de bord Cloudflare
    • Allez dans Travailleurs et pages et cliquez sur Créer un travailleur.
    • Donner un nom au travailleur (par exemple, edge-tts)
  2. Code de déploiement
    • Supprimer le code par défaut de l'éditeur
    • faire une copie de worker.js et collez le code dans le champ
    • Cliquez sur Enregistrer et déployer
  3. Définition de la clé API (facultatif)
    • Trouvez Paramètres -> Variables dans la page des paramètres du travailleur.
    • Cliquez sur Add variable, remplissez API_KEY pour le nom et la valeur de la clé que vous voulez.
    • Cliquez sur Enregistrer et déployer
  4. Configurer un nom de domaine personnalisé (optionnel)
    • Prérequis : Votre domaine est déjà hébergé sur Cloudflare et les enregistrements DNS pour le domaine ont été mandatés par Cloudflare (le statut du mandat est nuage orange).
    • Étapes de configuration :
      • Cliquez sur l'onglet Paramètres de la page Détails du travailleur.
      • Localisez la section Domaine et routage et cliquez sur le bouton Ajouter.
      • Sélectionnez Domaine personnalisé et saisissez le nom de domaine que vous souhaitez utiliser (par exemple, tts.example.com).
      • Cliquez sur Ajouter un domaine et attendez que le déploiement du certificat soit terminé (généralement en quelques minutes).

Utilisation

  1. TTY (interface texte-parole)
    • Exemple de discours en chinois :
     curl -X POST https://你的worker地址/v1/audio/speech \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer your-api-key" \
    -d '{
    "model": "tts-1",
    "input": "你好,世界!",
    "voice": "zh-CN-XiaoxiaoNeural",
    "response_format": "mp3",
    "speed": 1.0,
    "pitch": 1.0,
    "style":"general"
    }' --output chinese.mp3
    
    • Exemple de discours en anglais :
     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!",
    "voice": "en-US-JennyNeural",
    "response_format": "mp3",
    "speed": 1.0,
    "pitch": 1.0,
    "style":"general"
    }' --output english.mp3
    
  2. Utilisation du script de test
    • Télécharger le script de test test_voices.sh
    • Ajouter des autorisations d'exécution au script : bash
      chmod +x test_voices.sh
    • Exécutez le script : bash
      ./test_voices.sh <Worker地址> [API密钥]
    • Exemple : bash
      # 使用 API 密钥
      ./test_voices.sh https://your-worker.workers.dev your-api-key
      # 不使用 API 密钥
      ./test_voices.sh https://your-worker.workers.dev
    • Le script génère des fichiers audio de test pour chaque voix prise en charge, que vous pouvez écouter pour sélectionner la voix la plus appropriée.

Paramètre API Description

  • model (chaîne) : nom du modèle (valeur fixe), par exemple tts-1
  • input (chaîne) : le texte à convertir, par exemple "你好,世界!"
  • voice (chaîne) : nom de la voix, par exemple zh-CN-XiaoxiaoNeural
  • response_format (chaîne, facultatif) : format de sortie, la valeur par défaut est mp3
  • speed (nombre, optionnel) : débit de parole (0.5-2.0), valeur par défaut de 1.0
  • pitch (nombre, facultatif) : tonalité (0,5-2,0), la valeur par défaut est 1.0
  • style (chaîne, facultatif) : émotion, par défaut general

Liste des voix prises en charge

Veillez à utiliser le texte de la langue correspondant à la voix, par exemple la voix chinoise doit être utilisée avec le texte chinois. Voici des exemples de voix couramment utilisées :

  • zh-CN-XiaoxiaoNeuralXiaoxiao - Chaleureuse et vivante
  • zh-CN-XiaoyiNeuralXiaoyi - Chaleur et gentillesse
  • zh-CN-YunxiNeural: : Yumshi - voix d'homme, régulière
  • zh-CN-YunyangNeural: : Yun Yang - voix masculine, professionnelle
  • zh-CN-XiaohanNeuralXiaohan - Natural Flow (flux naturel)
  • zh-CN-XiaomengNeuralXiaomeng - Douceur et vitalité
  • zh-CN-XiaochenNeuralXiaochen - Douceur et facilité
  • Attendez...

 

code workers.js

const API_KEY = 'aisharenet'; // 替换为你的实际 API key
const TOKEN_REFRESH_BEFORE_EXPIRY = 3 * 60; // Token 刷新时间(秒)
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 `
        
        
        
        
        


 

Outil de synthèse vocale

 

 

 


 

 

 

 

Génération de la voix, veuillez patienter...

 

`; }
© déclaration de droits d'auteur
AiPPT

Articles connexes

Pas de commentaires

Vous devez être connecté pour participer aux commentaires !
S'inscrire maintenant
aucun
Pas de commentaires...