/** * Voice prompt utility for the timer. * * 4 fixed, polished prompts live in `cloudfunctions/tts`. We call that * cloud function with a key, get back an audio URL (cached in cloud * storage after first synthesis), and play it via createInnerAudioContext. * * The in-memory `_urlCache` avoids hitting the cloud function more than * once per prompt per session. The audio context is a singleton so * overlapping prompts interrupt each other cleanly. * * If the cloud function is unconfigured (no TTS credentials) and returns * `success: false`, we fall back to a silent no-op — better than spamming * the user with toast errors mid-plank. */ const PROMPTS = { halfway: '已完成一半啦,坚持就是胜利!', last30: '最后30秒,保持呼吸,稳住姿势!', last10: '最后10秒,再加把劲!', complete: '太棒了!今天的目标已完成,继续加油!' } let _ctx = null const _urlCache = {} // { promptKey: audioUrl } const _getCtx = () => { if (!_ctx) { _ctx = wx.createInnerAudioContext() // Play even when the device is in silent mode — we want the user to // actually hear training cues during a session. _ctx.obeyMuteSwitch = false } return _ctx } const _fetchUrl = async (promptKey) => { if (_urlCache[promptKey]) return _urlCache[promptKey] try { const res = await wx.cloud.callFunction({ name: 'tts', data: { promptKey }, // Default WeChat cloud timeout is 3s, which is exactly the TTS // first-call budget (synth + upload + tempURL). Bump to 30s for // cold starts; cached calls return in <500ms. config: { timeout: 30000 } }) if (res && res.result && res.result.success && res.result.audioUrl) { _urlCache[promptKey] = res.result.audioUrl return res.result.audioUrl } } catch (e) { // cloud function not deployed or other error — silent fallback } return null } /** * Play a prompt. Returns a Promise that resolves when playback starts * (or immediately if there's no audio available). */ const play = async (promptKey) => { if (!PROMPTS[promptKey]) return const url = await _fetchUrl(promptKey) if (!url) return const ctx = _getCtx() try { ctx.stop() ctx.src = url ctx.play() } catch (e) { /* ignore playback errors */ } } /** * Stop any currently-playing voice prompt. Call this when the user * pauses/stops/finishes training to avoid stray audio. */ const stop = () => { if (_ctx) { try { _ctx.stop() } catch (e) { /* ignore */ } } } /** * Tear down the audio context. Call on page unload. */ const destroy = () => { if (_ctx) { try { _ctx.destroy() } catch (e) { /* ignore */ } _ctx = null } } module.exports = { play, stop, destroy, PROMPTS }