feat: 新增运动节拍训练工具套件与子工具页面

新增运动节拍RepClock全套小程序页面、后端API与数据库服务,新增subhub子工具汇总页面,优化全局主页样式与配置,为web静态资源添加版本缓存参数,补充配套调试脚本
This commit is contained in:
yangxiangyuan
2026-09-09 14:43:06 +08:00
parent db7648eb4d
commit d4bf3627fd
69 changed files with 5759 additions and 297 deletions
+125
View File
@@ -0,0 +1,125 @@
const fs = require('fs')
const path = require('path')
const envPath = 'C:/Users/Administrator/Toolbox_local_creds.env.local'
if (fs.existsSync(envPath)) {
const txt = fs.readFileSync(envPath, 'utf-8')
txt.split(/\r?\n/).forEach(line => {
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/i)
if (!m) return
let v = m[2].trim()
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1)
if (!(m[1] in process.env)) process.env[m[1]] = v
})
}
process.chdir('d:/Trae_Files/TRAE-Toolbox')
global.__builtin_fetch = globalThis.fetch
const API_KEY = process.env.DASHSCOPE_API_KEY
const { getDb } = require(path.join(process.cwd(), 'src/server/mp_rep_clock/db'))
const tts = require(path.join(process.cwd(), 'src/server/mp_rep_clock/tts.js'))
console.log('=== 0. 自检 global/环境 ===')
console.log(' global.__builtin_fetch === globalThis.fetch:', global.__builtin_fetch === globalThis.fetch)
console.log(' typeof global.__builtin_fetch:', typeof global.__builtin_fetch)
console.log(' process.cwd():', process.cwd())
console.log(' TTS_ROOT:', tts.TTS_ROOT)
console.log(' TTS_ROOT exists:', fs.existsSync(tts.TTS_ROOT))
console.log(' apiKey.len:', API_KEY.length)
;(async () => {})()
async function main() {
console.log('\n=== 0.5 INLINE callCosyVoice 对比(与 debug_tts_inner.js 相同) ===')
const crypto = require('crypto')
const TTS_ROOT = tts.TTS_ROOT
const callCosyVoiceInline = async (text, voice) => {
const apiKey = process.env.DASHSCOPE_API_KEY || ''
const t = String(text || '').trim()
const isMale = voice.indexOf('male') >= 0
const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }
const fallbacks = [
{
name: 'Inline-A',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
body: { model: 'qwen3-tts-flash', input: { text: t, voice: 'Cherry', language_type: 'Chinese' } },
},
]
for (const fb of fallbacks) {
try {
const resp = await global.__builtin_fetch(fb.url, {
method: 'POST', headers, body: JSON.stringify(fb.body),
})
console.log(' inline fb', fb.name, 'status=', resp && resp.status)
if (!resp || !resp.ok) continue
const j = await resp.json()
const output = (j && j.output) || null
const audioUrl = output && output.audio && output.audio.url || ''
console.log(' inline fb', fb.name, 'audioUrl=', audioUrl.slice(0, 80))
if (!audioUrl) continue
const download = await global.__builtin_fetch(audioUrl)
if (!download || !download.ok) continue
const buf = Buffer.from(await download.arrayBuffer())
const storedName = `inline_${Date.now()}_${crypto.randomBytes(4).toString('hex')}.mp3`
const absPath = path.join(TTS_ROOT, storedName)
fs.writeFileSync(absPath, buf)
const durationMs = Math.max(1000, Math.round(buf.length / (12 * 1024 / 8)))
return { ok: true, url: `/uploads/mp_rep_clock/tts_cache/${storedName}`, durationMs }
} catch (e) { console.log(' inline ex', e.message) }
}
return { ok: false }
}
const inlineR = await callCosyVoiceInline('热身开合跳', 'female_01')
console.log(' inline result ok=', inlineR.ok, 'url=', inlineR.url)
console.log('\n=== 1. 检查 DASHSCOPE_API_KEY ===')
console.log(' len=', String(process.env.DASHSCOPE_API_KEY || '').length)
console.log()
console.log('=== 2. 先跑单条 synthesize 验证 Fallback ===')
const single = await tts.synthesize('开始训练,测试一下', 'female_01')
console.log(' single=', JSON.stringify(single, null, 2))
console.log()
console.log('=== 3. 全量 synthesizeBatch(title=测试一下) ===')
const card = {
title: '测试一下',
loopMode: 'sequential_once',
loopCount: 1,
announceLeadSeconds: '5,3',
steps: [
{ name: '热身开合跳', durationSec: 30, type: 'action' },
{ name: '平板支撑', durationSec: 60, type: 'action', restDurationSec: 15, voiceText: '' },
],
}
const batch = await tts.synthesizeBatch(card, 'female_01')
console.log(' batch total=', batch.total, 'success=', batch.success)
batch.results.forEach((r, i) => {
console.log(` [${i}] ok=${r.ok} cached=${r.cached} text=${JSON.stringify(r.text)} url=${r.url || ''} err=${r.error || ''}`)
})
console.log()
console.log('=== 4. TTS_ROOT uploads/mp_rep_clock/tts_cache files.Count ===')
const root = tts.TTS_ROOT
const files = fs.existsSync(root) ? fs.readdirSync(root).filter(f => f.endsWith('.mp3')) : []
console.log(' root=', root)
console.log(' files.Count=', files.length)
files.slice(-12).forEach(f => {
const stat = fs.statSync(path.join(root, f))
console.log(' -', f, stat.size, 'bytes')
})
console.log()
console.log('=== 5. rc_tts_cache 表行数(非 deleted)===')
try {
const row = getDb().prepare(`SELECT COUNT(*) as c FROM rc_tts_cache WHERE deleted = 0`).get()
console.log(' count=', row && row.c)
} catch (e) { console.log(' err=', e.message) }
console.log()
console.log('=== 6. 全部播报 lines 生成 ===')
const lines = tts.buildCardTTSBatch(card)
console.log(' lines=', lines.length)
lines.forEach((l, i) => console.log(` [${i}]`, l))
}
main().catch(e => console.error('FATAL', e))