perf(voice): 语音本地文件缓存 + 预加载,消除播放延迟
- 新增本地文件缓存层:downloadFile + saveFileSync 存持久本地文件,跨重启命中即出声 - _resolveSrc 三层优先:本地文件 -> 云函数 audioUrl -> 下载存本地 - 新增 preload(),timer 页 onLoad 后台预载全部语音 - 文件被清理自动回退重下载,下载失败回退临时 url Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+87
-3
@@ -29,8 +29,10 @@ const PROMPTS = {
|
||||
}
|
||||
|
||||
const FILEID_CACHE_KEY = 'tts_fileid_cache'
|
||||
const LOCAL_CACHE_KEY = 'tts_local_cache' // { cacheKey: savedFilePath } - 持久本地文件,跨重启
|
||||
|
||||
let _ctx = null
|
||||
let _localCache = null // { cacheKey: savedFilePath } - persisted, lazy-loaded
|
||||
const _urlCache = {} // { promptKey: audioUrl } — per-session, volatile
|
||||
let _fileIDCache = null // { promptKey: fileID } — persisted, lazy-loaded
|
||||
|
||||
@@ -47,6 +49,49 @@ const _saveFileIDCache = () => {
|
||||
try { wx.setStorageSync(FILEID_CACHE_KEY, _fileIDCache) } catch (e) {}
|
||||
}
|
||||
|
||||
const _loadLocalCache = () => {
|
||||
if (_localCache) return
|
||||
try {
|
||||
_localCache = wx.getStorageSync(LOCAL_CACHE_KEY) || {}
|
||||
} catch (e) {
|
||||
_localCache = {}
|
||||
}
|
||||
}
|
||||
|
||||
const _saveLocalCache = () => {
|
||||
try { wx.setStorageSync(LOCAL_CACHE_KEY, _localCache) } catch (e) {}
|
||||
}
|
||||
|
||||
// 本地文件可能被微信清理,播放前校验存在性
|
||||
const _fileExists = (path) => {
|
||||
if (!path) return false
|
||||
try {
|
||||
wx.getFileSystemManager().accessSync(path)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载网络音频并保存为持久本地文件,返回 savedFilePath;失败返回 null
|
||||
const _downloadAndSave = (url) => new Promise((resolve) => {
|
||||
wx.downloadFile({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.tempFilePath) {
|
||||
try {
|
||||
resolve(wx.getFileSystemManager().saveFileSync(res.tempFilePath))
|
||||
} catch (e) {
|
||||
resolve(null)
|
||||
}
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
},
|
||||
fail: () => resolve(null)
|
||||
})
|
||||
})
|
||||
|
||||
const _getCtx = () => {
|
||||
if (!_ctx) {
|
||||
_ctx = wx.createInnerAudioContext()
|
||||
@@ -114,14 +159,53 @@ const _fetchUrl = async (promptKey) => {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a playable src for a prompt, preferring a persisted local file
|
||||
* so playback is instant after the first download (across restarts).
|
||||
* Layers: local file cache -> cloud audioUrl -> download & save locally.
|
||||
*/
|
||||
const _resolveSrc = async (promptKey) => {
|
||||
const { voiceType, volume, speed } = _getTtsParams()
|
||||
const cacheKey = `${promptKey}:${voiceType}:${volume}:${speed}`
|
||||
|
||||
// 1. 本地文件缓存(持久,跨重启) - 命中即出声
|
||||
_loadLocalCache()
|
||||
if (_fileExists(_localCache[cacheKey])) return _localCache[cacheKey]
|
||||
|
||||
// 2. 取 audioUrl(fileID 缓存 / TTS 合成)
|
||||
const url = await _fetchUrl(promptKey)
|
||||
if (!url) return null
|
||||
|
||||
// 3. 下载并存本地,下次完全跳过云函数与下载
|
||||
const saved = await _downloadAndSave(url)
|
||||
if (saved) {
|
||||
_localCache[cacheKey] = saved
|
||||
_saveLocalCache()
|
||||
return saved
|
||||
}
|
||||
// 下载/保存失败,退回用临时 url 直接播
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台预载多段语音(解析+下载到本地),首次播放即可命中本地缓存。
|
||||
* 已缓存条目是 no-op,可重复调用。语音引导开启时在页面 onLoad 调用。
|
||||
*/
|
||||
const preload = (keys) => {
|
||||
if (!Array.isArray(keys)) keys = Object.keys(PROMPTS)
|
||||
keys.forEach((k) => {
|
||||
if (PROMPTS[k]) _resolveSrc(k).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 src = await _resolveSrc(promptKey)
|
||||
if (!src) return
|
||||
const ctx = _getCtx()
|
||||
try {
|
||||
ctx.stop()
|
||||
@@ -150,4 +234,4 @@ const destroy = () => {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { play, stop, destroy, PROMPTS }
|
||||
module.exports = { play, preload, stop, destroy, PROMPTS }
|
||||
|
||||
Reference in New Issue
Block a user