Browse Source

feat: add Azure Speech REST synthesis service

Stateless synthesize(text, signal) returning an MP3 Blob via
the Azure Speech REST API. Reads VITE_AZURE_SPEECH_KEY and
VITE_AZURE_SPEECH_REGION; documents both in .env.example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 2 tháng trước cách đây
mục cha
commit
db9b19a237
2 tập tin đã thay đổi với 55 bổ sung0 xóa
  1. 2 0
      .env.example
  2. 53 0
      src/views/Editor/EnglishSpeaking/services/speechService.ts

+ 2 - 0
.env.example

@@ -0,0 +1,2 @@
+VITE_AZURE_SPEECH_KEY=
+VITE_AZURE_SPEECH_REGION=

+ 53 - 0
src/views/Editor/EnglishSpeaking/services/speechService.ts

@@ -0,0 +1,53 @@
+const KEY = import.meta.env.VITE_AZURE_SPEECH_KEY as string | undefined
+const REGION = import.meta.env.VITE_AZURE_SPEECH_REGION as string | undefined
+const VOICE = 'en-US-AriaNeural'
+const FORMAT = 'audio-24khz-48kbitrate-mono-mp3'
+
+/**
+ * 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}'>${escapeXml(text)}</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 => ({
+    '<': '&lt;',
+    '>': '&gt;',
+    '&': '&amp;',
+    "'": '&apos;',
+    '"': '&quot;',
+  }[c]!))
+}