speechService.ts 1.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  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. const VOICE = 'en-US-AriaNeural'
  4. const FORMAT = 'audio-24khz-48kbitrate-mono-mp3'
  5. /**
  6. * Synthesize English text via Azure Speech REST.
  7. * Returns an MP3 Blob. Throws on credential / network / non-2xx.
  8. *
  9. * Pass an AbortSignal so callers (the audio player) can cancel
  10. * an in-flight synthesis when the user starts recording or
  11. * triggers a different playback.
  12. */
  13. export async function synthesize(text: string, signal?: AbortSignal): Promise<Blob> {
  14. if (!KEY || !REGION) {
  15. throw new Error('Azure Speech credentials not configured (VITE_AZURE_SPEECH_KEY / VITE_AZURE_SPEECH_REGION)')
  16. }
  17. const ssml =
  18. `<speak version='1.0' xml:lang='en-US'>` +
  19. `<voice name='${VOICE}'>${escapeXml(text)}</voice>` +
  20. `</speak>`
  21. const res = await fetch(
  22. `https://${REGION}.tts.speech.microsoft.com/cognitiveservices/v1`,
  23. {
  24. method: 'POST',
  25. signal,
  26. headers: {
  27. 'Ocp-Apim-Subscription-Key': KEY,
  28. 'Content-Type': 'application/ssml+xml',
  29. 'X-Microsoft-OutputFormat': FORMAT,
  30. 'User-Agent': 'PPT-EnglishSpeaking',
  31. },
  32. body: ssml,
  33. },
  34. )
  35. if (!res.ok) {
  36. throw new Error(`Azure TTS failed: ${res.status} ${res.statusText}`)
  37. }
  38. return res.blob()
  39. }
  40. function escapeXml(s: string): string {
  41. return s.replace(/[<>&'"]/g, c => ({
  42. '<': '&lt;',
  43. '>': '&gt;',
  44. '&': '&amp;',
  45. "'": '&apos;',
  46. '"': '&quot;',
  47. }[c]!))
  48. }