| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 |
- const KEY = import.meta.env.VITE_AZURE_SPEECH_KEY as string | undefined
- const REGION = import.meta.env.VITE_AZURE_SPEECH_REGION as string | undefined
- // 美式男声,口齿清晰、随和。备选(都是 en-US-*Neural):
- // GuyNeural / AndrewNeural / ChristopherNeural / TonyNeural(男)
- // JennyNeural / EmmaNeural / AvaNeural(女,清晰度也高)
- const VOICE = 'en-US-JennyNeural'
- const FORMAT = 'audio-24khz-48kbitrate-mono-mp3'
- // 默认偏自然偏快;调慢改 -10%、调更快改 +25% 之类。
- const RATE = '+10%'
- /**
- * Synthesize English text via Azure Speech REST.
- * Returns an MP3 Blob. Throws on credential / network / non-2xx.
- *
- * Pass an AbortSignal so callers (the audio player) can cancel
- * an in-flight synthesis when the user starts recording or
- * triggers a different playback.
- */
- export async function synthesize(text: string, signal?: AbortSignal): Promise<Blob> {
- if (!KEY || !REGION) {
- throw new Error('Azure Speech credentials not configured (VITE_AZURE_SPEECH_KEY / VITE_AZURE_SPEECH_REGION)')
- }
- const ssml =
- `<speak version='1.0' xml:lang='en-US'>` +
- `<voice name='${VOICE}'>` +
- `<prosody rate='${RATE}'>${escapeXml(text)}</prosody>` +
- `</voice>` +
- `</speak>`
- const res = await fetch(
- `https://${REGION}.tts.speech.microsoft.com/cognitiveservices/v1`,
- {
- method: 'POST',
- signal,
- headers: {
- 'Ocp-Apim-Subscription-Key': KEY,
- 'Content-Type': 'application/ssml+xml',
- 'X-Microsoft-OutputFormat': FORMAT,
- 'User-Agent': 'PPT-EnglishSpeaking',
- },
- body: ssml,
- },
- )
- if (!res.ok) {
- throw new Error(`Azure TTS failed: ${res.status} ${res.statusText}`)
- }
- return res.blob()
- }
- function escapeXml(s: string): string {
- return s.replace(/[<>&'"]/g, c => ({
- '<': '<',
- '>': '>',
- '&': '&',
- "'": ''',
- '"': '"',
- }[c]!))
- }
|