Files
wx_pbzc/cloudfunctions/tts/index.js
T
lc 1eff3d60c4 fix(ui): 多项 UI 修复 — 弹窗遮挡/按钮可见性/暗黑闪烁/云函数版本
- 计划编辑弹窗移出 .container 避免 fixed 降级 + 修复滚动
- dock bottom 40rpx→8rpx 紧贴底部, sheet padding 同步调整
- 计划编辑器取消/恢复默认按钮可见性修复(bg 匹配容器)
- ui-btn--danger 按钮可见性修复
- 清除确认弹窗跟随暗黑模式切换
- icons.js build() 中 Fill 路径覆盖 outlined 路径修复
- plan.js getPlanDay 忽略 customPlans 修复
- darkMode.js 冗余三元简化
- util.js fileToDataURI fallback 可读性提升
- theme.js: BASE_VARS 与 getThemeStyle 统一来源, 去 50ms navbar 延迟
- app.wxss: 移除 page transition 和 background-color 减少闪白
- 云函数 wx-server-sdk 版本统一 ~2.6.3, tts cloud.init 一致化
- 闪白问题: wx.setBackgroundColor 三层防护(onLaunch/applyThemeToPage/switchTab)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-10 09:40:04 +08:00

108 lines
4.1 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({ env: cloud.DYNAMIC_CURRENT_ENV })
// Polished, fixed prompts.
const PROMPTS = {
start: '训练已开始',
halfway: '已完成一半啦,坚持就是胜利!',
last30: '最后30秒,保持呼吸,稳住姿势!',
last10: '最后10秒,再加把劲!',
goal: '目标达成,继续挑战!',
complete: '太棒了!今天的目标已完成,继续加油!'
}
const CACHE_DIR = 'tts-cache'
const _synthesize = async (text, promptKey, opts) => {
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: (opts && opts.voiceType) || 101001, // 智瑜,温柔女声
Codec: 'mp3',
SampleRate: 16000,
Speed: (opts && opts.speed != null) ? opts.speed : 0,
Volume: (opts && opts.volume != null) ? opts.volume : 5
})
return Buffer.from(res.Audio, 'base64')
}
const _upload = async (key, buffer, voiceType) => {
const cloudPath = `${CACHE_DIR}/${key}-${voiceType || 'default'}.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, voiceType, volume, speed } = 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, { voiceType, volume, speed })
const newFileID = await _upload(promptKey, buffer, voiceType)
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 }
}
}