2e2594d154
- 修复 cloud.clearAll 静默失败导致重启后数据恢复 - 清除数据弹窗改用自定义 ui-modal 替代 wx.showModal - TTS 语音合成增加 fileID 持久化缓存,重启不再重新合成 - 排行榜限制前15名,maxRank 参数化到 config.js - 新增 config.js 统一管理版本号/更新日期/开发者/排行榜限制 - progress-ring 消除 getSystemInfoSync 弃用警告 - voice.js 增加 InnerAudioContext 错误监听 - storage/util 完善用户资料、打卡日期精度、记录日志
106 lines
3.8 KiB
JavaScript
106 lines
3.8 KiB
JavaScript
// Cloud function: synthesize the 4 fixed training-voice prompts.
|
|
//
|
|
// Caching strategy: the client stores the permanent `fileID` returned by
|
|
// the first successful synthesis in local storage. On subsequent calls it
|
|
// passes `fileID` back to us; we call `getTempFileURL` with it to get a
|
|
// fresh temporary URL instantly (no synthesis, no upload). If the cached
|
|
// fileID is stale (file deleted from cloud storage), we fall through and
|
|
// re-synthesize, returning the new fileID so the client can update its cache.
|
|
//
|
|
// Configuration (云开发控制台 → 云函数 → tts → 函数配置 → 环境变量):
|
|
// TTS_SECRET_ID — Tencent Cloud API key id
|
|
// TTS_SECRET_KEY — Tencent Cloud API key
|
|
// TTS_REGION — (optional) defaults to ap-guangzhou
|
|
//
|
|
// NOTE: env var names cannot start with SCF_ / QCLOUD_ / TENCENTCLOUD_ —
|
|
// those prefixes are reserved by Tencent Cloud SCF. Use the TTS_ prefix.
|
|
|
|
const cloud = require('wx-server-sdk')
|
|
cloud.init()
|
|
|
|
// Polished, fixed prompts.
|
|
const PROMPTS = {
|
|
halfway: '已完成一半啦,坚持就是胜利!',
|
|
last30: '最后30秒,保持呼吸,稳住姿势!',
|
|
last10: '最后10秒,再加把劲!',
|
|
complete: '太棒了!今天的目标已完成,继续加油!'
|
|
}
|
|
|
|
const CACHE_DIR = 'tts-cache'
|
|
|
|
const _synthesize = async (text, promptKey) => {
|
|
let TtsClient
|
|
try {
|
|
TtsClient = require('tencentcloud-sdk-nodejs').tts.v20190823.Client
|
|
} catch (e) {
|
|
throw new Error('tencentcloud-sdk-nodejs not installed. Run `npm i tencentcloud-sdk-nodejs` in this cloud function directory.')
|
|
}
|
|
|
|
const secretId = process.env.TTS_SECRET_ID
|
|
const secretKey = process.env.TTS_SECRET_KEY
|
|
if (!secretId || !secretKey) {
|
|
throw new Error('TTS_SECRET_ID / TTS_SECRET_KEY not configured.')
|
|
}
|
|
|
|
const client = new TtsClient({
|
|
credential: { secretId, secretKey },
|
|
region: process.env.TTS_REGION || 'ap-guangzhou'
|
|
})
|
|
|
|
const res = await client.TextToVoice({
|
|
Text: text,
|
|
SessionId: `${promptKey}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
VoiceType: 101001, // 智瑜,温柔女声
|
|
Codec: 'mp3',
|
|
SampleRate: 16000,
|
|
Speed: 0,
|
|
Volume: 5
|
|
})
|
|
return Buffer.from(res.Audio, 'base64')
|
|
}
|
|
|
|
const _upload = async (key, buffer) => {
|
|
const cloudPath = `${CACHE_DIR}/${key}.mp3`
|
|
// uploadFile returns the canonical fileID (e.g. "cloud://env.xxx/...").
|
|
// We must use THAT — not the relative cloudPath we passed in — when
|
|
// calling getTempFileURL. A self-constructed fileID is invalid.
|
|
const res = await cloud.uploadFile({ cloudPath, fileContent: buffer })
|
|
return res.fileID
|
|
}
|
|
|
|
exports.main = async (event) => {
|
|
const { promptKey, fileID } = event || {}
|
|
const text = PROMPTS[promptKey]
|
|
if (!text) {
|
|
return { success: false, error: `Unknown prompt key: ${promptKey}` }
|
|
}
|
|
|
|
// If the client passed a cached fileID, try to get a fresh temp URL
|
|
// from it — this is the fast path (no synthesis, no upload).
|
|
if (fileID) {
|
|
try {
|
|
const urlRes = await cloud.getTempFileURL({ fileList: [fileID] })
|
|
const url = urlRes.fileList[0].tempFileURL
|
|
if (url) {
|
|
return { success: true, audioUrl: url, fileID, text, cached: true }
|
|
}
|
|
} catch (e) {
|
|
// fileID stale or file deleted — fall through to re-synthesize
|
|
}
|
|
}
|
|
|
|
// Slow path: synthesize + upload + return fresh fileID for caching
|
|
try {
|
|
const buffer = await _synthesize(text, promptKey)
|
|
const newFileID = await _upload(promptKey, buffer)
|
|
const urlRes = await cloud.getTempFileURL({ fileList: [newFileID] })
|
|
const url = urlRes.fileList[0].tempFileURL
|
|
if (!url) {
|
|
return { success: false, error: 'getTempFileURL returned empty URL', text }
|
|
}
|
|
return { success: true, audioUrl: url, fileID: newFileID, text, cached: false }
|
|
} catch (e) {
|
|
return { success: false, error: e.message, text }
|
|
}
|
|
}
|