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
@@ -0,0 +1,58 @@
const path = require('path')
process.chdir('d:/Trae_Files/TRAE-Toolbox')
const http = require('http')
const db = require(path.join(process.cwd(), 'src/server/mp_rep_clock/db'))
const locks = require(path.join(process.cwd(), 'src/server/mp_rep_clock/locks'))
const store = require(path.join(process.cwd(), 'src/server/mp_rep_clock/kits/rep_clock/store'))
// 清锁先
locks.resetRunningLock()
const nowIso = () => new Date().toISOString()
db.getDb().prepare(`UPDATE rc_sessions SET status='abandoned', ended_at=?, updated_at=?, manual_stopped=1 WHERE status='running' AND deleted=0`).run(nowIso(), nowIso())
const cardId = 'rc_card_c6aebdfb-7330-43c1-b42c-a6a856a715f2'
// 1. 直接 Node 层 reserve + createSession,模拟"第一发start成功"
const s1 = store.createSession({ cardId })
const r1 = locks.reserveRunningLock(s1.id)
console.log('== 1st createSession OK ==')
console.log('session1 id:', s1.id, 'status=', s1.status)
console.log('lock reserve1:', JSON.stringify(r1))
// 2. 再调用一次 createSession(第二发 start 模拟冲突),但不清锁
const s2 = store.createSession({ cardId })
const r2 = locks.reserveRunningLock(s2.id)
console.log('\n== 2nd 故意冲突 ==')
console.log('session2 id:', s2.id)
console.log('lock reserve2:', JSON.stringify(r2))
console.log('字段检查:expiresInMs 存在?', typeof r2.expiresInMs, r2.expiresInMs, '秒=', Math.ceil(r2.expiresInMs/1000))
// 3. 再用 HTTP 调 sessions/start 直接看 409 body(等价于用户 curl)
console.log('\n== 3rd HTTP POST sessions/start ==')
const payload = JSON.stringify({ cardId })
const req = http.request({
hostname: '127.0.0.1', port: 8976,
path: '/api/mp_rep_clock/kits/rep_clock/sessions/start',
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(payload),
'X-Mp-Timeassistant': 'probe_v1',
}
}, (res) => {
let buf = ''
res.on('data', c => buf += c)
res.on('end', () => {
console.log('HTTP', res.statusCode)
console.log('BODY:', buf)
try {
const j = JSON.parse(buf)
console.log('字段检查 expiresInMs =', j.expiresInMs, '秒=', Math.ceil(j.expiresInMs/1000))
console.log('字段检查 conflictSessionId =', j.conflictSessionId)
console.log('字段检查 expiredLockCleared =', j.expiredLockCleared)
} catch(e){ console.log('not json:', e.message) }
})
})
req.on('error', e => console.log('err', e.message))
req.write(payload)
req.end()
@@ -0,0 +1,24 @@
const fs = require('fs')
const path = require('path')
process.chdir('d:/Trae_Files/TRAE-Toolbox')
const db = require(path.join(process.cwd(), 'src/server/mp_rep_clock/db'))
const locks = require(path.join(process.cwd(), 'src/server/mp_rep_clock/locks.js'))
console.log('=== 1. 锁表当前值 ===')
console.log(JSON.stringify(locks.getLockState(), null, 2))
console.log('\n=== 2. rc_sessions 最近 5 条 ===')
const rows = db.getDb().prepare(`SELECT id, status, card_id, ended_at, created_at, updated_at FROM rc_sessions WHERE deleted=0 ORDER BY datetime(created_at) DESC LIMIT 5`).all()
rows.forEach(r => console.log(' -', JSON.stringify(r)))
console.log('\n=== 3. 直接调 reserveRunningLock 看新版逻辑是否加载(看是否返回 expiresInMs + expiredLockCleared)===')
const testSid = 'rc_session_test_' + Math.random().toString(36).slice(2, 10)
const result = locks.reserveRunningLock(testSid, 'rep_clock')
console.log(JSON.stringify(result, null, 2))
console.log('新版字段校验:result.expiresInMs =', result && result.expiresInMs, typeof result && result.expiresInMs)
console.log('新版字段校验:result.expiredLockCleared =', result && result.expiredLockCleared, typeof result && result.expiredLockCleared)
console.log('\n=== 4. 回滚:删除测试锁(如果创建成功) ===')
if (result && result.ok) locks.resetRunningLock()
console.log('回滚后锁表:', JSON.stringify(locks.getLockState()))
@@ -0,0 +1,25 @@
const fs = require('fs')
const path = require('path')
process.chdir('d:/Trae_Files/TRAE-Toolbox')
const db = require(path.join(process.cwd(), 'src/server/mp_rep_clock/db'))
const locks = require(path.join(process.cwd(), 'src/server/mp_rep_clock/locks.js'))
console.log('=== 锁表当前值 ===')
console.log(JSON.stringify(locks.getLockState(), null, 2))
console.log('\n=== rc_sessions status=running 行数 ===')
const cnt = db.getDb().prepare(`SELECT COUNT(*) as c FROM rc_sessions WHERE status='running' AND deleted=0`).get()
console.log('running cnt=', cnt.c)
const rows = db.getDb().prepare(`SELECT id, status, card_id, created_at, updated_at FROM rc_sessions WHERE status='running' AND deleted=0 ORDER BY datetime(created_at) DESC LIMIT 5`).all()
rows.forEach(r => console.log(' -', JSON.stringify(r)))
console.log('\n=== 立即操作:清锁 + 所有 running 转 abandoned ===')
locks.resetRunningLock()
const nowIso = () => new Date().toISOString()
db.getDb().prepare(`UPDATE rc_sessions SET status='abandoned', ended_at=?, updated_at=?, manual_stopped=1 WHERE status='running' AND deleted=0`).run(nowIso(), nowIso())
console.log('\n=== 操作后复检 ===')
console.log('锁表:', JSON.stringify(locks.getLockState(), null, 2))
const cnt2 = db.getDb().prepare(`SELECT COUNT(*) as c FROM rc_sessions WHERE status='running' AND deleted=0`).get()
console.log('running cnt=', cnt2.c, '(应为 0)')
@@ -0,0 +1,33 @@
const http = require('http')
const data = JSON.stringify({ cardId: 'rc_card_c6aebdfb-7330-43c1-b42c-a6a856a715f2' })
const req = http.request({
hostname: '127.0.0.1', port: 8976,
path: '/api/mp_rep_clock/kits/rep_clock/sessions/start',
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(data),
'Accept': 'application/json; charset=utf-8',
'X-Mp-Timeassistant': 'smoke_test_probe_xxx',
}
}, (res) => {
let buf = ''
res.on('data', c => buf += c)
res.on('end', () => {
console.log('HTTP', res.statusCode)
console.log('BODY:', buf)
try {
const j = JSON.parse(buf)
console.log('字段检查:')
console.log(' ok =', j.ok)
console.log(' error =', j.error)
console.log(' conflictSessionId =', j.conflictSessionId)
console.log(' conflict_session_id =', j.conflict_session_id)
console.log(' expiresInMs =', j.expiresInMs, typeof j.expiresInMs)
console.log(' expires_in_ms =', j.expires_in_ms, typeof j.expires_in_ms)
} catch (e) { console.log('not json', e.message) }
})
})
req.on('error', e => console.log('err:', e.message))
req.write(data)
req.end()
@@ -0,0 +1,45 @@
const fs = require('fs')
const path = require('path')
process.chdir('d:/Trae_Files/TRAE-Toolbox')
const db = require(path.join(process.cwd(), 'src/server/mp_rep_clock/db'))
const locks = require(path.join(process.cwd(), 'src/server/mp_rep_clock/locks.js'))
const store = require(path.join(process.cwd(), 'src/server/mp_rep_clock/kits/rep_clock/store.js'))
async function main() {
console.log('=== 修复前 ===')
console.log('锁表:', JSON.stringify(locks.getLockState(), null, 2))
const list0 = db.getDb().prepare(`SELECT id, status, card_id, created_at, updated_at FROM rc_sessions WHERE deleted = 0 ORDER BY datetime(created_at) DESC LIMIT 5`).all()
console.log('rc_sessions 最近5条:')
list0.forEach(r => console.log(' -', JSON.stringify(r)))
console.log()
console.log('=== 立即清空锁 + 把所有 status=running 的 session 转 abandoned ===')
locks.resetRunningLock()
db.getDb().prepare(`UPDATE rc_sessions SET status='abandoned', ended_at=?, updated_at=?, manual_stopped=1 WHERE status='running' AND deleted=0`).run(new Date().toISOString(), new Date().toISOString())
console.log()
console.log('=== 修复后再次尝试等价 sessionStart 链路 ===')
const list = store.listCards()
const card = (list && list[0]) || null
if (card) {
console.log('选中 card=', card.id, card.title)
const session = store.createSession({ cardId: card.id })
console.log('创建 session id=', session.id)
const r = locks.reserveRunningLock(session.id)
console.log('reserveRunningLock=', JSON.stringify(r))
console.log('锁表=', JSON.stringify(locks.getLockState()))
const sessionsAfter = db.getDb().prepare(`SELECT id, status, card_id, updated_at FROM rc_sessions WHERE deleted = 0 ORDER BY datetime(updated_at) DESC LIMIT 3`).all()
console.log('sessions 最后3条:')
sessionsAfter.forEach(s => console.log(' -', JSON.stringify(s)))
}
console.log()
console.log('=== 额外验证:故意再调一次 sessionStart 制造 409 冲突,看 expiresInMs 是否带回 ===')
if (card) {
const otherSession = store.createSession({ cardId: card.id })
const conflict = locks.reserveRunningLock(otherSession.id)
console.log('冲突时 reserveRunningLock=', JSON.stringify(conflict))
}
}
main().catch(e => console.error(e))
@@ -0,0 +1,60 @@
const http = require('http')
const BASE = { host: 'localhost', port: 5054, headers: { 'X-Mp-Timeassistant': 'mp_node_smoke_ok', 'Content-Type': 'application/json; charset=utf-8', 'Accept': 'application/json; charset=utf-8' } }
function req(method, path, bodyObj) {
return new Promise((resolve, reject) => {
const bodyBuf = bodyObj ? Buffer.from(JSON.stringify(bodyObj), 'utf8') : null
const req = http.request({ ...BASE, method, path, headers: { ...BASE.headers, 'Content-Length': bodyBuf ? bodyBuf.length : 0 } }, (res) => {
let data = []
res.on('data', (c) => data.push(c))
res.on('end', () => {
const text = Buffer.concat(data).toString('utf8')
try { resolve({ status: res.statusCode, headers: res.headers, body: text ? JSON.parse(text) : null, text }) } catch (e) { resolve({ status: res.statusCode, headers: res.headers, text }) }
})
})
req.on('error', reject)
if (bodyBuf) req.write(bodyBuf)
req.end()
})
}
;(async () => {
console.log('\n=== 1. GET /api/mp_time_assistant/meta (kits应=2 不应有hub_only中转) ===')
const r1 = await req('GET', '/api/mp_time_assistant/meta')
console.log('status', r1.status, 'CT', r1.headers['content-type'])
console.log('ok=', r1.body.ok, 'kits.Count=', r1.body.kits ? r1.body.kits.length : 0)
if (r1.body.kits) r1.body.kits.forEach((k) => console.log(' kit:', JSON.stringify(k)))
console.log('\n=== 2. POST /cards 新建中文卡 title="今天练背日" (UTF-8 JSON) ===')
const r2 = await req('POST', '/api/mp_rep_clock/kits/rep_clock/cards', {
title: '今天练背日',
loopMode: 'loop_n_times',
loopCount: 3,
steps: [
{ name: '高位下拉', durationSec: 60, restSec: 20, type: 'action' },
{ name: '坐姿划船', durationSec: 60, restSec: 20, type: 'action' },
],
})
console.log('status', r2.status, 'CT=', r2.headers['content-type'])
const c = r2.body.card
console.log('card.id=', c.id)
console.log('card.title (JSON)=', JSON.stringify(c.title), ' codePoints=', Array.from(c.title).map((ch) => ch.charCodeAt(0).toString(16)).join(' '))
console.log('card.loopMode=', c.loopMode, 'loopTimes=', c.loopTimes, 'voice=', c.voice, 'ttsReady=', c.ttsReady)
console.log('card.steps[0]: name=', JSON.stringify(c.steps[0].name), 'restSec=', c.steps[0].restSec)
console.log('card.steps[1]: name=', JSON.stringify(c.steps[1].name), 'restSec=', c.steps[1].restSec)
const id = c.id
console.log('\n=== 3. GET /cards 再查回刚写入的中文卡 ===')
const r3 = await req('GET', '/api/mp_rep_clock/kits/rep_clock/cards')
const found = (r3.body.cards || r3.body.data || []).find((x) => x.id === id)
console.log('found=', !!found)
if (found) {
console.log('title=', JSON.stringify(found.title))
console.log('loopTimes=', found.loopTimes, ' voice=', found.voice, ' ttsReady=', found.ttsReady)
found.steps.forEach((s, i) => console.log(` step[${i}] name=${JSON.stringify(s.name)} restSec=${s.restSec}`))
}
console.log('\n=== 4. GET personal_collect 独立前缀 独立DB是否联通 ===')
const r4 = await req('GET', '/api/mp_personal_collect/kits/personal_collect/items')
console.log('status', r4.status, 'ok=', r4.body.ok, 'items/data.Count=', (r4.body.items || r4.body.data || []).length)
console.log('\n=== ALL SMOKE PASSED if title=中文正常显示 ===')
})().catch((e) => { console.error('ERR', e); process.exit(1) })
@@ -0,0 +1,58 @@
const http = require('http')
const BASE = { host: 'localhost', port: 5053, headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Mp-Timeassistant': 'mp_node_client_enc_test_xxx' } }
const post = (path, obj) => new Promise((res, rej) => {
const body = Buffer.from(JSON.stringify(obj), 'utf8')
const req = http.request({ ...BASE, method: 'POST', path, headers: { ...BASE.headers, 'Content-Length': body.length } }, (r) => {
let data = []
r.on('data', (c) => data.push(c))
r.on('end', () => { try { res({ status: r.statusCode, headers: r.headers, body: JSON.parse(Buffer.concat(data).toString('utf8')) }) } catch (e) { rej(e) } })
})
req.on('error', rej)
req.write(body); req.end()
})
const get = (path) => new Promise((res, rej) => {
http.get({ ...BASE, path }, (r) => {
let data = []
r.on('data', (c) => data.push(c))
r.on('end', () => { try { res({ status: r.statusCode, body: JSON.parse(Buffer.concat(data).toString('utf8')) }) } catch (e) { rej(e) } })
}).on('error', rej)
})
;(async () => {
console.log('=== Step1: POST /cards via UTF-8 JSON (like WeChat mini-program) ===')
const body = {
title: '瑜伽垫上腰部运动',
loopMode: 'loop_n_times',
loopCount: 2,
steps: [
{ name: '热身动作', durationSec: 30, restSec: 10, type: 'action' },
{ name: '平板支撑', durationSec: 45, restSec: 15, type: 'action' },
],
}
const pr = await post('/api/mp_rep_clock/kits/rep_clock/cards', body)
console.log('POST status=', pr.status)
const c = pr.body.card
console.log('card.id:', c.id)
console.log('card.title (JSON.stringify):', JSON.stringify(c.title), ' chars:', Array.from(c.title).map((ch) => ch.charCodeAt(0).toString(16).padStart(4, '0')).join(' '))
console.log('card.loopTimes:', c.loopTimes, ' voice:', c.voice, ' ttsReady:', c.ttsReady)
console.log('card.steps:')
c.steps.forEach((s, i) => console.log(` [${i}] name=${JSON.stringify(s.name)} durationSec=${s.durationSec} restSec=${s.restSec} type=${s.type}`))
console.log('\n=== Step2: GET /cards ===')
const gr = await get('/api/mp_rep_clock/kits/rep_clock/cards')
const c0 = gr.body.cards[0]
console.log('cards[0].title=', JSON.stringify(c0.title))
console.log('cards[0].ttsReady=', c0.ttsReady, ' loopTimes=', c0.loopTimes, ' voice=', c0.voice)
c0.steps.forEach((s, i) => console.log(` [${i}] name=${JSON.stringify(s.name)} restSec=${s.restSec}`))
console.log('\n=== Step3: HEX dump directly from DB to prove storage ===')
const Database = require('better-sqlite3')
const path = require('path')
const db = new Database(path.join(process.cwd(), 'data', 'mp_rep_clock.db'))
const row = db.prepare(`SELECT id, hex(title) ht, hex(steps_json) hsj FROM rc_cards WHERE id=? ORDER BY rowid DESC LIMIT 1`).get(c.id)
console.log('DB hex(title):', row.ht.substring(0, 200))
const titleFromDb = Buffer.from(row.ht, 'hex').toString('utf8')
console.log('DB decoded title:', JSON.stringify(titleFromDb))
const stepsParsed = JSON.parse(Buffer.from(row.hsj, 'hex').toString('utf8'))
stepsParsed.forEach((s, i) => console.log(` DB steps[${i}] name=${JSON.stringify(s.name)} restSec=${s.restSec}`))
console.log('\n=== ALL ENCODING CHECKS PASSED if title decoded correctly above ===')
})().catch((e) => { console.error('ERR', e); process.exit(1) })
@@ -0,0 +1,61 @@
const fs = require('fs')
const path = require('path')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
if (fs.existsSync(envPath)) {
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
}
process.chdir('d:/Trae_Files/TRAE-Toolbox')
global.__builtin_fetch = globalThis.fetch
const db = require(path.join(process.cwd(), 'src/server/mp_rep_clock/db'))
const locks = require(path.join(process.cwd(), 'src/server/mp_rep_clock/locks.js'))
const store = require(path.join(process.cwd(), 'src/server/mp_rep_clock/kits/rep_clock/store.js'))
async function main() {
console.log('=== 1. 当前锁表 rc_global_lock ===')
try {
const lock = locks.getLockState()
console.log(' lock=', JSON.stringify(lock, null, 2))
} catch (e) { console.log(' err', e.message) }
console.log()
console.log('=== 2. 找一个已有卡片(标题=测试一下)===')
const list = store.listCards()
console.log(' 卡片总数=', list && list.length)
let card = null
if (list && list.length) {
card = list.find(c => String(c.title || '').indexOf('测试一下') >= 0) || list[0]
console.log(' 选中 card=', card && card.id, card && card.title)
}
console.log()
console.log('=== 3. 调 store.createSession + locks.reserveRunningLock(等价 sessionStart 接口)===')
try {
const session = store.createSession({ cardId: card && card.id })
console.log(' createSession ok, id=', session && session.id)
const r = locks.reserveRunningLock(session.id)
console.log(' reserveRunningLock=', JSON.stringify(r))
if (!r.ok) {
console.log(' ★★★ 锁冲突!!调用 resetRunningLock 清空后再试一次')
locks.resetRunningLock()
console.log(' 清空后锁状态=', JSON.stringify(locks.getLockState()))
const r2 = locks.reserveRunningLock(session.id)
console.log(' 第二次 reserveRunningLock=', JSON.stringify(r2))
}
} catch (e) { console.log(' ex=', e.stack || e.message) }
console.log()
console.log('=== 4. 查看 sessions 表最后 3 行 ===')
try {
const rows = db.getDb().prepare(`SELECT id, card_id, status, created_at FROM rc_sessions WHERE deleted = 0 ORDER BY datetime(created_at) DESC LIMIT 3`).all()
rows.forEach(r => console.log(' -', JSON.stringify(r)))
} catch (e) { console.log(' err', e.message) }
}
main()
+38
View File
@@ -0,0 +1,38 @@
const fs = require('fs')
const path = require('path')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
if (fs.existsSync(envPath)) {
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
}
global.__builtin_fetch = globalThis.fetch
const API_KEY = process.env.DASHSCOPE_API_KEY
;(async () => {
const url = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/text-to-audio'
const body = {
model: 'cosyvoice-v1',
input: { text: '热身开合跳' },
parameters: { voice: 'longruoxi_v1', format: 'mp3', sample_rate: 24000, volume: 1.0, speech_rate: 1.0 },
}
try {
const resp = await global.__builtin_fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'X-DashScope-Async': 'enable',
},
body: JSON.stringify(body),
})
console.log('status=', resp.status)
console.log('headers=', [...resp.headers.entries()].filter(([k]) => k.startsWith('x-dashscope') || k.startsWith('content') || k.startsWith('x-request') || k === 'date').map(([k, v]) => k + '=' + v).join(' '))
const txt = await resp.text()
console.log('body=\n' + txt)
} catch (e) { console.error('ERR', e && e.message ? e.message : e) }
})()
+49
View File
@@ -0,0 +1,49 @@
const path = require('path')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
const fs = require('fs')
if (fs.existsSync(envPath)) {
process.env.USERPROFILE_CREDS = envPath
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
console.log('[env] loaded Toolbox_local_creds.env.local from', envPath)
} else {
console.log('[env] NOT found', envPath)
}
console.log('[env] DASHSCOPE_API_KEY =', (process.env.DASHSCOPE_API_KEY || '').slice(0, 14) + '... len=' + (process.env.DASHSCOPE_API_KEY || '').length)
global.__builtin_fetch = globalThis.fetch
if (!global.__builtin_fetch) try { global.__builtin_fetch = require('node-fetch') } catch (_) { console.error('no fetch available') }
const tts = require('../../src/server/mp_rep_clock/tts')
;(async () => {
const card = {
title: '测试一下',
loopMode: 'loop_till_stop',
loopCount: 0,
announceLeadSeconds: '5,3,1',
steps: [
{ name: '热身开合跳', durationSec: 30, restSec: 10, type: 'action' },
{ name: '平板支撑', durationSec: 45, restSec: 15, type: 'action' },
],
}
console.log('\n=== 1. 看 buildCardTTSBatch lines ===')
const lines = tts.buildCardTTSBatch(card)
console.log('lines.length=', lines.length)
lines.forEach((l, i) => console.log(` [${i}] ${l}`))
console.log('\n=== 2. 合成第0条 (开始训练,测试一下) ===')
const s0 = await tts.synthesize(lines[0], 'female_01')
console.log('synthesize ok=', s0.ok, 'error=', s0.error || null, 'url=', s0.url || null, 'cached=', !!s0.cached, 'durationMs=', s0.durationMs)
console.log('\n=== 3. 全量 synthesizeBatch ===')
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} err=${r.error || null} text=${r.text.slice(0, 20)} url=${(r.url || '').slice(0, 60)}`))
console.log('\n=== 4. 检查 uploads 目录是否生成了 mp3 ===')
try { const f = fs.readdirSync(tts.TTS_ROOT); console.log('TTS_ROOT=', tts.TTS_ROOT, 'files.Count=', f.length); f.slice(0, 10).forEach((x) => console.log(' ', x, Math.round((fs.statSync(path.join(tts.TTS_ROOT, x)).size / 1024)), 'KB')) } catch (e) { console.log('ERR read dir', e && e.message) }
})().catch(e => { console.error('FATAL', e); process.exit(1) })
@@ -0,0 +1,45 @@
// 一次性探针:验证 DashScope CosyVoice(SpeechSynthesizer) 的正确 model/voice 参数组合
// 用法:node dev_test_scripts/debug/debug_tts_cosy_probe.js
const fs = require('fs')
const path = require('path')
const userProfile = process.env.USERPROFILE || process.env.HOME || require('os').homedir()
const credsPath = path.join(userProfile, 'Toolbox_local_creds.env.local')
if (fs.existsSync(credsPath)) {
fs.readFileSync(credsPath, 'utf-8').split(/\r?\n/).forEach(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const idx = trimmed.indexOf('=')
if (idx <= 0) return
const key = trimmed.slice(0, idx).trim()
const val = trimmed.slice(idx + 1).trim()
if (key && !Object.prototype.hasOwnProperty.call(process.env, key)) process.env[key] = val
})
}
const apiKey = process.env.DASHSCOPE_API_KEY || ''
const CASES = [
['cosyvoice-v3-flash/longanyang (当前配置)', { model: 'cosyvoice-v3-flash', input: { text: '测试一下', voice: 'longanyang', format: 'mp3', sample_rate: 24000 } }],
['cosyvoice-v2/Cherry', { model: 'cosyvoice-v2', input: { text: '测试一下', voice: 'Cherry', format: 'mp3', sample_rate: 24000 } }],
['cosyvoice-v2/longxiaoxue', { model: 'cosyvoice-v2', input: { text: '测试一下', voice: 'longxiaoxue', format: 'mp3', sample_rate: 24000 } }],
['cosyvoice-v1/Cherry', { model: 'cosyvoice-v1', input: { text: '测试一下', voice: 'Cherry', format: 'mp3', sample_rate: 24000 } }],
['cosyvoice-v2-flash/Cherry', { model: 'cosyvoice-v2-flash', input: { text: '测试一下', voice: 'Cherry', format: 'mp3', sample_rate: 24000 } }],
]
;(async () => {
for (const [name, body] of CASES) {
const t0 = Date.now()
try {
const resp = await fetch('https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer', {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const txt = await resp.text().catch(() => '')
const ok = resp.ok && txt.includes('http')
console.log(`[${ok ? '✔' : '✘'}]`, name, 'HTTP', resp.status, (Date.now() - t0) + 'ms', (resp.ok ? txt.slice(0, 120) : txt.slice(0, 200)).replace(/\s+/g, ' '))
} catch (e) {
console.log('[✘]', name, 'fetch 异常:', String(e && e.message || e))
}
}
process.exit(0)
})()
@@ -0,0 +1,37 @@
// 一次性 DB 维护:恢复被 INSERT OR IGNORE + deleted=1 阻塞的 3 条缓存,
// 清理无 DB 引用的孤儿音频文件。用法:node dev_test_scripts/debug/debug_tts_db_fix.js
const fs = require('fs')
const path = require('path')
const db = require('better-sqlite3')('data/mp_rep_clock.db')
const FIX = {
'还有5秒': 'rc_1788857916346_c9bb0cc4.wav',
'还有3秒': 'rc_1788857916424_0d8f7e90.wav',
'本次训练结束,辛苦了': 'rc_1788857916507_e06bd852.wav',
}
let recovered = 0
for (const [text, file] of Object.entries(FIX)) {
const abs = path.join(process.cwd(), 'uploads', 'mp_rep_clock', 'tts_cache', file)
if (!fs.existsSync(abs)) { console.log('[skip 文件不存在]', file); continue }
const info = db.prepare('UPDATE rc_tts_cache SET deleted=0, url=? WHERE text=? AND voice=?')
.run('/uploads/mp_rep_clock/tts_cache/' + file, text, 'female_01')
console.log('[fix]', text, '->', file, 'affected rows:', info.changes)
recovered += info.changes
}
// 清理有文件但没有任何 active 缓存行引用的孤儿
const activeUrls = db.prepare('SELECT url FROM rc_tts_cache WHERE deleted=0').all().map(r => r.url)
const cacheDir = path.join(process.cwd(), 'uploads', 'mp_rep_clock', 'tts_cache')
let orphans = 0
for (const f of fs.readdirSync(cacheDir)) {
const rel = '/uploads/mp_rep_clock/tts_cache/' + f
if (!activeUrls.includes(rel)) {
try { fs.unlinkSync(path.join(cacheDir, f)); orphans++; console.log('[orphan-del]', f) } catch (_) {}
}
}
console.log('[result] 恢复缓存行:', recovered, '; 删除孤儿文件:', orphans)
console.log('[result] active 缓存条数:', db.prepare('SELECT COUNT(*) c FROM rc_tts_cache WHERE deleted=0').get().c)
console.log('[result] active 缓存列表:')
db.prepare('SELECT text, url FROM rc_tts_cache WHERE deleted=0 ORDER BY text').all().forEach(r => console.log(' ', r.text, '=>', r.url))
+79
View File
@@ -0,0 +1,79 @@
const fs = require('fs')
const path = require('path')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
if (fs.existsSync(envPath)) {
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
}
process.chdir('d:/Trae_Files/TRAE-Toolbox')
global.__builtin_fetch = globalThis.fetch
const crypto = require('crypto')
const apiKey = process.env.DASHSCOPE_API_KEY
const t = '热身开合跳'
const headers = {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
}
const CANDIDATES = []
CANDIDATES.push({
name: 'Fallback-A:Qwen3-TTS',
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' },
},
})
CANDIDATES.push({
name: 'Fallback-B:CosyVoice-v3',
url: 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer',
body: {
model: 'cosyvoice-v3-flash',
input: { text: t, voice: 'longanyang', format: 'mp3', sample_rate: 24000 },
},
})
async function run() {
for (const c of CANDIDATES) {
console.log('\n======== try', c.name, '========')
try {
const resp = await global.__builtin_fetch(c.url, {
method: 'POST',
headers,
body: JSON.stringify(c.body),
})
console.log('status=', resp.status, 'OK=', resp.ok)
const txt = await resp.text()
console.log('text head=', txt.slice(0, 800))
if (resp.ok) {
try {
const j = JSON.parse(txt)
let audioUrl = ''
const output = (j && j.output) || null
if (output) {
if (output.audio && typeof output.audio.url === 'string') audioUrl = output.audio.url
}
console.log('audioUrl=', audioUrl)
if (audioUrl) {
const dl = await global.__builtin_fetch(audioUrl)
console.log('download status=', dl.status, 'len=', dl.headers && dl.headers.get ? dl.headers.get('content-length') : '?')
if (dl.ok) {
const buf = Buffer.from(await dl.arrayBuffer())
const p = path.join(process.env.TEMP || '.', 'tts_dl_' + Date.now() + '.mp3')
fs.writeFileSync(p, buf)
console.log('saved to', p, 'bytes=', buf.length)
}
}
} catch (e) { console.log('json parse err', e.message) }
}
} catch (e) { console.log('ex', e && e.message ? e.message : e) }
}
}
run()
+115
View File
@@ -0,0 +1,115 @@
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
if (fs.existsSync(envPath)) {
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
}
process.chdir('d:/Trae_Files/TRAE-Toolbox')
global.__builtin_fetch = globalThis.fetch
const TTS_ROOT = path.join(process.cwd(), 'uploads', 'mp_rep_clock', 'tts_cache')
if (!fs.existsSync(TTS_ROOT)) fs.mkdirSync(TTS_ROOT, { recursive: true })
const VOICE_ID_DEFAULT = 'female_01'
const callCosyVoice = async (text, voice = VOICE_ID_DEFAULT) => {
const apiKey = process.env.DASHSCOPE_API_KEY || ''
console.log('[callCosyVoice] apiKey.len=', apiKey.length, 'text=', JSON.stringify(text), 'voice=', voice)
if (!apiKey) return { ok: false, error: 'DASHSCOPE_API_KEY 未配置' }
const t = String(text || '').trim()
if (!t) return { ok: false, error: 'empty text' }
const isMale = voice.indexOf('male') >= 0
const headers = {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
}
const fallbacks = []
fallbacks.push({
name: 'Fallback-A:Qwen3-TTS',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
body: {
model: 'qwen3-tts-flash',
input: {
text: t,
voice: isMale ? 'Cherry' : 'Cherry',
language_type: 'Chinese',
},
},
})
fallbacks.push({
name: 'Fallback-B:CosyVoice-v3',
url: 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer',
body: {
model: 'cosyvoice-v3-flash',
input: {
text: t,
voice: isMale ? 'longxiaocheng' : 'longanyang',
format: 'mp3',
sample_rate: 24000,
},
},
})
for (let idx = 0; idx < fallbacks.length; idx++) {
const fb = fallbacks[idx]
console.log(' [fb ' + idx + '] try ' + fb.name + ' url=' + fb.url)
console.log(' [fb ' + idx + '] body=', JSON.stringify(fb.body))
try {
const resp = await global.__builtin_fetch(fb.url, {
method: 'POST',
headers,
body: JSON.stringify(fb.body),
})
console.log(' [fb ' + idx + '] resp.status=', resp ? resp.status : 'null', 'resp.ok=', resp ? resp.ok : 'null')
if (!resp || !resp.ok) {
const txt = resp ? await resp.text().catch(() => '') : ''
console.log(' [fb ' + idx + '] !ok resp.text head=', (txt || '').slice(0, 500))
continue
}
const j = await resp.json()
console.log(' [fb ' + idx + '] json keys=', Object.keys(j || {}).join(','))
let audioUrl = ''
try {
const output = (j && j.output) || null
console.log(' [fb ' + idx + '] output keys=', output ? Object.keys(output).join(',') : 'null')
if (output) {
if (output.audio && typeof output.audio.url === 'string') { audioUrl = output.audio.url; console.log(' [fb ' + idx + '] audioUrl via output.audio.url') }
if (!audioUrl && typeof output.audio_url === 'string') { audioUrl = output.audio_url; console.log(' [fb ' + idx + '] audioUrl via output.audio_url') }
if (!audioUrl && typeof output.audioUrl === 'string') { audioUrl = output.audioUrl; console.log(' [fb ' + idx + '] audioUrl via output.audioUrl') }
if (!audioUrl && Array.isArray(output.results) && output.results[0]) {
const first = output.results[0] || {}
if (first.audio && typeof first.audio.url === 'string') { audioUrl = first.audio.url; console.log(' [fb ' + idx + '] audioUrl via output.results[0].audio.url') }
}
}
} catch (e) { console.log(' [fb ' + idx + '] output ex=', e.message) }
console.log(' [fb ' + idx + '] audioUrl=', audioUrl)
if (!audioUrl) continue
const download = await global.__builtin_fetch(audioUrl)
console.log(' [fb ' + idx + '] download status=', download ? download.status : 'null')
if (!download || !download.ok) continue
const buf = Buffer.from(await download.arrayBuffer())
const storedName = `rc_${Date.now()}_${crypto.randomBytes(4).toString('hex')}.mp3`
const absPath = path.join(TTS_ROOT, storedName)
fs.writeFileSync(absPath, buf)
const relPath = `uploads/mp_rep_clock/tts_cache/${storedName}`
const stat = fs.statSync(absPath)
const durationMs = Math.max(1000, Math.round(stat.size / (12 * 1024 / 8)))
console.log(' [fb ' + idx + '] OK storedName=' + storedName + ' size=' + stat.size + ' durationMs=' + durationMs)
return { ok: true, url: `/${relPath}`, durationMs }
} catch (e) {
console.log(' [fb ' + idx + '] EXCEPTION=', e && e.stack ? e.stack : String(e))
}
}
return { ok: false, error: 'All TTS fallbacks failed' }
}
;(async () => {
console.log('=== callCosyVoice(热身开合跳, female_01) ===')
const r = await callCosyVoice('热身开合跳', 'female_01')
console.log('\nFINAL RESULT=', JSON.stringify(r, null, 2))
})()
@@ -0,0 +1,39 @@
// 一次性探针:本地直连 DashScope 复现 TTS 合成,定位“合成中”卡住/失败的真实原因
// 用法:node dev_test_scripts/debug/debug_tts_live_probe.js
const fs = require('fs')
const path = require('path')
// 与 src/server/index.js 一致的 creds 加载(只读 key 名不含值打印)
const userProfile = process.env.USERPROFILE || process.env.HOME || require('os').homedir()
const credsPath = path.join(userProfile, 'Toolbox_local_creds.env.local')
if (fs.existsSync(credsPath)) {
fs.readFileSync(credsPath, 'utf-8').split(/\r?\n/).forEach(line => {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
const idx = trimmed.indexOf('=')
if (idx <= 0) return
const key = trimmed.slice(0, idx).trim()
const val = trimmed.slice(idx + 1).trim()
if (key && !Object.prototype.hasOwnProperty.call(process.env, key)) process.env[key] = val
})
}
console.log('[probe] DASHSCOPE_API_KEY 已加载:', !!process.env.DASHSCOPE_API_KEY, process.env.DASHSCOPE_API_KEY ? '(len=' + process.env.DASHSCOPE_API_KEY.length + ')' : '')
const tts = require('../../src/server/mp_rep_clock/tts.js')
const text = process.argv[2] || '下一组'
const voice = process.argv[3] || 'female_01'
const timer = setTimeout(() => { console.log('[probe] 超时 60s 强制退出'); process.exit(2) }, 60000)
;(async () => {
const t0 = Date.now()
const r = await tts.synthesize(text, voice)
console.log('[probe] synthesize(', JSON.stringify(text), ',', voice, ') 结果=', JSON.stringify(r), '耗时', Date.now() - t0, 'ms')
if (r && r.url) {
const abs = path.join(process.cwd(), String(r.url).replace(/^\/+/, '').replace(/\//g, path.sep))
if (fs.existsSync(abs)) console.log('[probe] 文件存在:', abs, fs.statSync(abs).size, 'bytes')
}
clearTimeout(timer)
process.exit(r && r.ok ? 0 : 1)
})().catch(e => { console.error('[probe] 异常:', e); clearTimeout(timer); process.exit(1) })
@@ -0,0 +1,70 @@
const fs = require('fs')
const path = require('path')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
if (fs.existsSync(envPath)) {
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
}
global.__builtin_fetch = globalThis.fetch
const API_KEY = process.env.DASHSCOPE_API_KEY
const CASES = [
{
name: 'T1 model=qwen-audio-3.0-tts-flash, voice=longanyang, text2audio/audio-synthesis',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/audio-synthesis',
headers: { 'X-DashScope-Async': 'enable' },
body: { model: 'qwen-audio-3.0-tts-flash', input: { text: '热身开合跳' }, parameters: { voice: 'longanyang', format: 'mp3', sample_rate: 24000 } }
},
{
name: 'T2 model=cosyvoice-v3-flash, voice=longanyang',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/audio-synthesis',
headers: { 'X-DashScope-Async': 'enable' },
body: { model: 'cosyvoice-v3-flash', input: { text: '热身开合跳' }, parameters: { voice: 'longanyang', format: 'mp3', sample_rate: 24000 } }
},
{
name: 'T3 model=cosyvoice-v3-plus, voice=longruoxi',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/audio-synthesis',
headers: { 'X-DashScope-Async': 'enable' },
body: { model: 'cosyvoice-v3-plus', input: { text: '热身开合跳' }, parameters: { voice: 'longruoxi', format: 'mp3', sample_rate: 24000 } }
},
{
name: 'T4 qwen-audio-3.0-tts-plus, voice=longruoxi_v1',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/audio-synthesis',
headers: { 'X-DashScope-Async': 'enable' },
body: { model: 'qwen-audio-3.0-tts-plus', input: { text: '热身开合跳' }, parameters: { voice: 'longruoxi_v1', format: 'mp3', sample_rate: 24000 } }
},
]
async function runCase(c) {
console.log('\n======== ' + c.name + ' ========')
try {
const resp = await global.__builtin_fetch(c.url, {
method: 'POST',
headers: Object.assign({ 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, c.headers || {}),
body: JSON.stringify(c.body),
})
console.log('status=', resp.status, 'CT=', resp.headers.get('content-type'))
const j = await resp.json()
console.log(JSON.stringify(j).slice(0, 600))
if (j && j.output && j.output.task_id) {
console.log('task_id=', j.output.task_id, '— poll 2 次看看:')
for (let k = 0; k < 2; k++) {
await new Promise(r => setTimeout(r, 2000))
const poll = await global.__builtin_fetch('https://dashscope.aliyuncs.com/api/v1/tasks/' + j.output.task_id, { headers: { 'Authorization': `Bearer ${API_KEY}` } })
const p = await poll.json()
console.log(' poll k=', k, 'status=', p && p.output && p.output.task_status, 'url=', p && p.output && p.output.audio_url && String(p.output.audio_url).slice(0, 80), 'msg=', p && p.message && String(p.message).slice(0, 80))
if (p && p.output && (p.output.task_status === 'SUCCEEDED' || p.output.task_status === 'FAILED' || p.output.audio_url)) break
}
}
} catch (e) { console.error('ERR', e && e.message ? e.message : e) }
}
;(async () => {
for (const c of CASES) await runCase(c)
})()
+62
View File
@@ -0,0 +1,62 @@
const fs = require('fs')
const path = require('path')
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
if (fs.existsSync(envPath)) {
const text = fs.readFileSync(envPath, 'utf8')
for (const line of text.split(/\r?\n/)) {
if (!line || /^\s*#/.test(line)) continue
const i = line.indexOf('=')
if (i < 0) continue
const k = line.slice(0, i).trim(), v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
if (k && !(k in process.env)) process.env[k] = v
}
}
global.__builtin_fetch = globalThis.fetch
const API_KEY = process.env.DASHSCOPE_API_KEY
const CANDIDATES = []
CANDIDATES.push({
name: 'A1 Qwen-TTS 推荐 Fallback-A:multimodal-generation/generation (model=qwen3-tts-flash, voice=Cherry, 同步audio.url)',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
body: { model: 'qwen3-tts-flash', input: { text: '热身开合跳', voice: 'Cherry', language_type: 'Chinese' } }
})
CANDIDATES.push({
name: 'A2 qwen-audio-3.0-tts-flash Cherry',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
body: { model: 'qwen-audio-3.0-tts-flash', input: { text: '热身开合跳', voice: 'Cherry', language_type: 'Chinese' } }
})
CANDIDATES.push({
name: 'A3 qwen3-tts-flash 中文女声 Xiaobei',
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
body: { model: 'qwen3-tts-flash', input: { text: '热身开合跳,加油坚持。', voice: 'Xiaobei', language_type: 'Chinese' } }
})
CANDIDATES.push({
name: 'A4 CosyVoice WorkspaceId 端点占位(有 WorkspaceId 就填,否则直接走 Fallback A)',
url: 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer',
body: { model: 'cosyvoice-v3-flash', input: { text: '热身开合跳', voice: 'longanyang', format: 'mp3', sample_rate: 24000 } }
})
;(async () => {
for (const c of CANDIDATES) {
console.log('\n======== 试 ' + c.name + ' ========')
try {
const resp = await global.__builtin_fetch(c.url, {
method: 'POST',
headers: Object.assign({ 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' }, c.headers || {}),
body: JSON.stringify(c.body),
})
console.log('status=', resp.status, 'CT=', resp.headers.get('content-type'))
const ct = resp.headers.get('content-type') || ''
if (ct.indexOf('application/json') >= 0) {
const j = await resp.json()
console.log('json=', JSON.stringify(j).slice(0, 500))
} else {
const buf = Buffer.from(await resp.arrayBuffer())
console.log('bytes=', buf.length, 'HEAD=', [...buf.slice(0, 4)].map(b => b.toString(16)).join(' '))
const p = path.join(process.env.TEMP || '.', 'tts_test_' + Date.now() + '.mp3')
fs.writeFileSync(p, buf)
console.log('saved mp3=', p, 'sizeKB=', Math.round(buf.length/1024))
}
} catch (e) { console.error('ERR', e && e.message ? e.message : e) }
}
})()
+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))