speechService.ts 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. const KEY = import.meta.env.VITE_AZURE_SPEECH_KEY as string | undefined
  2. const REGION = import.meta.env.VITE_AZURE_SPEECH_REGION as string | undefined
  3. // 美式男声,口齿清晰、随和。备选(都是 en-US-*Neural):
  4. // GuyNeural / AndrewNeural / ChristopherNeural / TonyNeural(男)
  5. // JennyNeural / EmmaNeural / AvaNeural(女,清晰度也高)
  6. const VOICE = 'en-US-JennyNeural'
  7. const FORMAT = 'audio-24khz-48kbitrate-mono-mp3'
  8. // 默认偏自然偏快;调慢改 -10%、调更快改 +25% 之类。
  9. const RATE = '+10%'
  10. /**
  11. * Synthesize English text via Azure Speech REST.
  12. * Returns an MP3 Blob. Throws on credential / network / non-2xx.
  13. *
  14. * Pass an AbortSignal so callers (the audio player) can cancel
  15. * an in-flight synthesis when the user starts recording or
  16. * triggers a different playback.
  17. */
  18. export async function synthesize(text: string, signal?: AbortSignal): Promise<Blob> {
  19. if (!KEY || !REGION) {
  20. throw new Error('Azure Speech credentials not configured (VITE_AZURE_SPEECH_KEY / VITE_AZURE_SPEECH_REGION)')
  21. }
  22. const ssml =
  23. `<speak version='1.0' xml:lang='en-US'>` +
  24. `<voice name='${VOICE}'>` +
  25. `<prosody rate='${RATE}'>${escapeXml(text)}</prosody>` +
  26. `</voice>` +
  27. `</speak>`
  28. const res = await fetch(
  29. `https://${REGION}.tts.speech.microsoft.com/cognitiveservices/v1`,
  30. {
  31. method: 'POST',
  32. signal,
  33. headers: {
  34. 'Ocp-Apim-Subscription-Key': KEY,
  35. 'Content-Type': 'application/ssml+xml',
  36. 'X-Microsoft-OutputFormat': FORMAT,
  37. 'User-Agent': 'PPT-EnglishSpeaking',
  38. },
  39. body: ssml,
  40. },
  41. )
  42. if (!res.ok) {
  43. throw new Error(`Azure TTS failed: ${res.status} ${res.statusText}`)
  44. }
  45. return res.blob()
  46. }
  47. function escapeXml(s: string): string {
  48. return s.replace(/[<>&'"]/g, c => ({
  49. '<': '&lt;',
  50. '>': '&gt;',
  51. '&': '&amp;',
  52. "'": '&apos;',
  53. '"': '&quot;',
  54. }[c]!))
  55. }