- Add continuous guided light-body-scan meditation (16 segments, 97% coverage) - Edge TTS API endpoint with Xiaoxiao neural voice at -40% rate - Pre-generated chime WAVs and guidance MP3s in public/audio/ - Mute toggle in practice page, sequenced non-overlapping audio - Fix middleware to serve /audio and /api/audio/tts without auth - All LLM output localized to Chinese; fix blank home/settings pages - DeepSeek API compat: json_object, increased max_tokens - Analytics, cron reminders, onboarding flow fixes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
51 lines
1.6 KiB
TypeScript
51 lines
1.6 KiB
TypeScript
// Audio playback using pre-generated files + server-side TTS API.
|
|
// All functions silently no-op when audio playback fails.
|
|
|
|
function playFile(path: string) {
|
|
try {
|
|
const audio = new Audio(path);
|
|
audio.play().catch(() => {});
|
|
} catch {}
|
|
}
|
|
|
|
export function playStartChime() {
|
|
playFile("/audio/start-chime.wav");
|
|
}
|
|
|
|
export function playTick() {
|
|
playFile("/audio/tick.wav");
|
|
}
|
|
|
|
export function playEndChime() {
|
|
playFile("/audio/end-chime.wav");
|
|
}
|
|
|
|
export function playMidGuidance(lang: string) {
|
|
const file = lang === "zh" ? "mid-guidance-zh.mp3" : "mid-guidance-en.mp3";
|
|
playFile(`/audio/${file}`);
|
|
}
|
|
|
|
export function playEndGuidance(lang: string) {
|
|
const file = lang === "zh" ? "end-guidance-zh.mp3" : "end-guidance-en.mp3";
|
|
playFile(`/audio/${file}`);
|
|
}
|
|
|
|
// Dynamic TTS via server-side Edge TTS (neural voices) API.
|
|
// Falls back silently on error (network issue, edge-tts not installed, etc.).
|
|
export async function speakText(text: string, lang: string = "zh"): Promise<void> {
|
|
try {
|
|
const url = `/api/audio/tts?text=${encodeURIComponent(text)}&lang=${lang}&rate=-40%25`;
|
|
const response = await fetch(url);
|
|
if (!response.ok) return;
|
|
const blob = await response.blob();
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const audio = new Audio(blobUrl);
|
|
audio.onended = () => { URL.revokeObjectURL(blobUrl); resolve(); };
|
|
audio.onerror = () => { URL.revokeObjectURL(blobUrl); resolve(); };
|
|
audio.play().catch(() => { URL.revokeObjectURL(blobUrl); resolve(); });
|
|
});
|
|
} catch {}
|
|
}
|