|
|
@@ -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 => ({
|
|
|
+ '<': '<',
|
|
|
+ '>': '>',
|
|
|
+ '&': '&',
|
|
|
+ "'": ''',
|
|
|
+ '"': '"',
|
|
|
+ }[c]!))
|
|
|
+}
|