fix: use AudioContext for voice previews to bypass browser autoplay policy
new Audio().play() after an async await loses the user gesture context and is silently blocked. Creating AudioContext synchronously before the fetch preserves the permission, then decode/play through it after the await. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -524,7 +524,6 @@ const voiceBlend = ref<VoiceBlendEntry[]>([
|
|||||||
]);
|
]);
|
||||||
const blendPreviewText = ref("Hello! I'm your assistant. How can I help you today?");
|
const blendPreviewText = ref("Hello! I'm your assistant. How can I help you today?");
|
||||||
const blendPreviewing = ref(false);
|
const blendPreviewing = ref(false);
|
||||||
let _blendAudioUrl = "";
|
|
||||||
|
|
||||||
function addBlendSlot() {
|
function addBlendSlot() {
|
||||||
voiceBlend.value.push({ voice: "af_heart", weight: 1.0 });
|
voiceBlend.value.push({ voice: "af_heart", weight: 1.0 });
|
||||||
@@ -537,6 +536,8 @@ function removeBlendSlot(idx: number) {
|
|||||||
async function previewBlend() {
|
async function previewBlend() {
|
||||||
if (blendPreviewing.value || !blendPreviewText.value.trim()) return;
|
if (blendPreviewing.value || !blendPreviewText.value.trim()) return;
|
||||||
blendPreviewing.value = true;
|
blendPreviewing.value = true;
|
||||||
|
// Create AudioContext synchronously in user-gesture context to bypass autoplay policy
|
||||||
|
const ctx = new AudioContext();
|
||||||
try {
|
try {
|
||||||
const blob = await synthesiseSpeech(
|
const blob = await synthesiseSpeech(
|
||||||
blendPreviewText.value,
|
blendPreviewText.value,
|
||||||
@@ -544,10 +545,11 @@ async function previewBlend() {
|
|||||||
voiceTtsSpeed.value,
|
voiceTtsSpeed.value,
|
||||||
voiceBlend.value,
|
voiceBlend.value,
|
||||||
);
|
);
|
||||||
if (_blendAudioUrl) URL.revokeObjectURL(_blendAudioUrl);
|
const buf = await ctx.decodeAudioData(await blob.arrayBuffer());
|
||||||
_blendAudioUrl = URL.createObjectURL(blob);
|
const src = ctx.createBufferSource();
|
||||||
const audio = new Audio(_blendAudioUrl);
|
src.buffer = buf;
|
||||||
audio.play();
|
src.connect(ctx.destination);
|
||||||
|
src.start(0);
|
||||||
} catch {
|
} catch {
|
||||||
toastStore.show("Preview failed — TTS may not be ready", "error");
|
toastStore.show("Preview failed — TTS may not be ready", "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -572,20 +574,23 @@ async function loadVoiceTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const voicePreviewing = ref(false);
|
const voicePreviewing = ref(false);
|
||||||
let _voicePreviewUrl = "";
|
|
||||||
|
|
||||||
async function previewVoice() {
|
async function previewVoice() {
|
||||||
if (voicePreviewing.value) return;
|
if (voicePreviewing.value) return;
|
||||||
voicePreviewing.value = true;
|
voicePreviewing.value = true;
|
||||||
|
// Create AudioContext synchronously in user-gesture context to bypass autoplay policy
|
||||||
|
const ctx = new AudioContext();
|
||||||
try {
|
try {
|
||||||
const blob = await synthesiseSpeech(
|
const blob = await synthesiseSpeech(
|
||||||
"Hello! I'm your assistant. How can I help you today?",
|
"Hello! I'm your assistant. How can I help you today?",
|
||||||
voiceTtsVoice.value,
|
voiceTtsVoice.value,
|
||||||
voiceTtsSpeed.value,
|
voiceTtsSpeed.value,
|
||||||
);
|
);
|
||||||
if (_voicePreviewUrl) URL.revokeObjectURL(_voicePreviewUrl);
|
const buf = await ctx.decodeAudioData(await blob.arrayBuffer());
|
||||||
_voicePreviewUrl = URL.createObjectURL(blob);
|
const src = ctx.createBufferSource();
|
||||||
new Audio(_voicePreviewUrl).play();
|
src.buffer = buf;
|
||||||
|
src.connect(ctx.destination);
|
||||||
|
src.start(0);
|
||||||
} catch {
|
} catch {
|
||||||
toastStore.show("Preview failed — TTS may not be ready", "error");
|
toastStore.show("Preview failed — TTS may not be ready", "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user