voice-training-smoke-test.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env node
  2. /**
  3. * 语音克隆培训包 - 冒烟测试
  4. *
  5. * 测试场景:
  6. * 1. 检查培训包结构完整性
  7. * 2. 模拟语音服务调用(不真实发送)
  8. * 3. 验证 Fmode 后端连通性
  9. */
  10. const path = require('path');
  11. const fs = require('fs');
  12. const ROOT = path.join(__dirname, '..');
  13. const TRAINING_DIR = path.join(ROOT, 'dist/qiwei-training');
  14. console.log('🧪 语音克隆培训包 - 冒烟测试\n');
  15. // ============================================================
  16. // 测试 1:培训包结构完整性
  17. // ============================================================
  18. function test1_PackageStructure() {
  19. console.log('【测试 1】检查培训包结构...');
  20. const required = [
  21. 'qiwei-workbench.exe',
  22. '.env.local',
  23. 'README.md',
  24. 'qiwei.runtime.config.mjs',
  25. 'web',
  26. 'knowledge',
  27. ];
  28. const missing = required.filter(item => {
  29. const fullPath = path.join(TRAINING_DIR, item);
  30. return !fs.existsSync(fullPath);
  31. });
  32. if (missing.length > 0) {
  33. console.error(` ❌ 缺少文件/目录: ${missing.join(', ')}`);
  34. return false;
  35. }
  36. const exeSize = (fs.statSync(path.join(TRAINING_DIR, 'qiwei-workbench.exe')).size / (1024 * 1024)).toFixed(1);
  37. console.log(` ✅ 结构完整,exe 大小: ${exeSize} MB`);
  38. return true;
  39. }
  40. // ============================================================
  41. // 测试 2:README 语音说明
  42. // ============================================================
  43. function test2_READMEContent() {
  44. console.log('\n【测试 2】检查 README 语音说明...');
  45. const readme = fs.readFileSync(path.join(TRAINING_DIR, 'README.md'), 'utf8');
  46. const requiredSections = [
  47. '## 语音克隆功能',
  48. '录制参考音频',
  49. '初始化声音档案',
  50. 'SILK 格式',
  51. 'Fmode 飞马余额',
  52. ];
  53. const missing = requiredSections.filter(section => !readme.includes(section));
  54. if (missing.length > 0) {
  55. console.error(` ❌ README 缺少说明: ${missing.join(', ')}`);
  56. return false;
  57. }
  58. console.log(' ✅ README 包含完整语音使用说明');
  59. return true;
  60. }
  61. // ============================================================
  62. // 测试 3:语音服务模块完整性
  63. // ============================================================
  64. function test3_VoiceServiceModule() {
  65. console.log('\n【测试 3】检查语音服务模块...');
  66. const voiceService = path.join(ROOT, 'mcp/src/core/voice-clone-service.js');
  67. if (!fs.existsSync(voiceService)) {
  68. console.error(' ❌ 语音服务模块不存在');
  69. return false;
  70. }
  71. const content = fs.readFileSync(voiceService, 'utf8');
  72. const criticalComponents = [
  73. 'class VoiceCloneService',
  74. 'async synthesize',
  75. 'async encodeSilk',
  76. 'server.fmode.cn/api/voice/indextts2',
  77. '@binsee/wx-voice',
  78. 'ffmpeg',
  79. ];
  80. const missing = criticalComponents.filter(comp => !content.includes(comp));
  81. if (missing.length > 0) {
  82. console.error(` ❌ 语音服务缺少组件: ${missing.join(', ')}`);
  83. return false;
  84. }
  85. console.log(' ✅ 语音服务模块完整');
  86. return true;
  87. }
  88. // ============================================================
  89. // 测试 4:语音依赖声明
  90. // ============================================================
  91. function test4_VoiceDependencies() {
  92. console.log('\n【测试 4】检查语音依赖...');
  93. const packageJson = require(path.join(ROOT, 'package.json'));
  94. const voiceDeps = [
  95. '@ffmpeg-installer/ffmpeg',
  96. '@ffprobe-installer/ffprobe',
  97. '@binsee/wx-voice',
  98. ];
  99. const allDeps = {
  100. ...packageJson.dependencies,
  101. ...packageJson.optionalDependencies,
  102. };
  103. const missing = voiceDeps.filter(dep => !allDeps[dep]);
  104. if (missing.length > 0) {
  105. console.error(` ❌ package.json 缺少依赖: ${missing.join(', ')}`);
  106. return false;
  107. }
  108. console.log(' ✅ 语音依赖已声明');
  109. voiceDeps.forEach(dep => {
  110. console.log(` - ${dep}: ${allDeps[dep]}`);
  111. });
  112. return true;
  113. }
  114. // ============================================================
  115. // 测试 5:MCP 工具注册
  116. // ============================================================
  117. function test5_MCPToolsRegistration() {
  118. console.log('\n【测试 5】检查 MCP 语音工具注册...');
  119. const serverJs = path.join(ROOT, 'mcp/src/server.js');
  120. if (!fs.existsSync(serverJs)) {
  121. console.error(' ❌ MCP server.js 不存在');
  122. return false;
  123. }
  124. const content = fs.readFileSync(serverJs, 'utf8');
  125. const voiceTools = [
  126. 'qiwei_enroll_voice',
  127. 'qiwei_voice_profile_status',
  128. 'qiwei_send_cloned_voice',
  129. ];
  130. const missing = voiceTools.filter(tool => !content.includes(tool));
  131. if (missing.length > 0) {
  132. console.error(` ❌ 缺少 MCP 工具: ${missing.join(', ')}`);
  133. return false;
  134. }
  135. console.log(' ✅ MCP 语音工具已注册');
  136. return true;
  137. }
  138. // ============================================================
  139. // 测试 6:Fmode 后端连通性(可选)
  140. // ============================================================
  141. async function test6_FmodeBackendConnectivity() {
  142. console.log('\n【测试 6】检查 Fmode 后端连通性...');
  143. // 读取 .env.local 中的 token(如果存在)
  144. const envPath = path.join(TRAINING_DIR, '.env.local');
  145. const envContent = fs.readFileSync(envPath, 'utf8');
  146. const tokenMatch = envContent.match(/QIWEI_AUTH_TOKEN=(.+)/);
  147. if (!tokenMatch || !tokenMatch[1] || tokenMatch[1].trim() === '') {
  148. console.log(' ⚠️ 跳过(未配置 QIWEI_AUTH_TOKEN)');
  149. return true;
  150. }
  151. const token = tokenMatch[1].trim();
  152. try {
  153. const https = require('https');
  154. const endpoint = 'https://server.fmode.cn/api/product/balance';
  155. const result = await new Promise((resolve, reject) => {
  156. const req = https.get(endpoint, {
  157. headers: {
  158. 'Authorization': `Bearer ${token}`,
  159. },
  160. }, (res) => {
  161. let data = '';
  162. res.on('data', chunk => data += chunk);
  163. res.on('end', () => {
  164. if (res.statusCode === 200) {
  165. resolve(JSON.parse(data));
  166. } else {
  167. reject(new Error(`HTTP ${res.statusCode}: ${data}`));
  168. }
  169. });
  170. });
  171. req.on('error', reject);
  172. req.setTimeout(5000, () => {
  173. req.destroy();
  174. reject(new Error('连接超时'));
  175. });
  176. });
  177. console.log(' ✅ Fmode 后端连通');
  178. console.log(` 余额: ${result.balance || 'N/A'} 元`);
  179. return true;
  180. } catch (err) {
  181. console.error(` ❌ Fmode 后端连接失败: ${err.message}`);
  182. return false;
  183. }
  184. }
  185. // ============================================================
  186. // 主测试流程
  187. // ============================================================
  188. async function main() {
  189. const tests = [
  190. { name: 'PackageStructure', fn: test1_PackageStructure, required: true },
  191. { name: 'READMEContent', fn: test2_READMEContent, required: true },
  192. { name: 'VoiceServiceModule', fn: test3_VoiceServiceModule, required: true },
  193. { name: 'VoiceDependencies', fn: test4_VoiceDependencies, required: true },
  194. { name: 'MCPToolsRegistration', fn: test5_MCPToolsRegistration, required: true },
  195. { name: 'FmodeBackendConnectivity', fn: test6_FmodeBackendConnectivity, required: false },
  196. ];
  197. let passed = 0;
  198. let failed = 0;
  199. let skipped = 0;
  200. for (const test of tests) {
  201. try {
  202. const result = await test.fn();
  203. if (result) {
  204. passed++;
  205. } else {
  206. failed++;
  207. if (test.required) {
  208. console.error(`\n❌ 必要测试失败: ${test.name}`);
  209. }
  210. }
  211. } catch (err) {
  212. console.error(` ❌ 测试异常: ${err.message}`);
  213. if (test.required) {
  214. failed++;
  215. } else {
  216. skipped++;
  217. }
  218. }
  219. }
  220. console.log('\n' + '='.repeat(60));
  221. console.log(`测试完成: ${passed} 通过, ${failed} 失败, ${skipped} 跳过`);
  222. console.log('='.repeat(60));
  223. if (failed > 0) {
  224. console.error('\n❌ 冒烟测试失败,请检查上述错误');
  225. process.exit(1);
  226. } else {
  227. console.log('\n✅ 冒烟测试通过!培训包可以部署。');
  228. console.log('\n下一步:');
  229. console.log(' 1. 拷贝 dist/qiwei-training/ 到培训机');
  230. console.log(' 2. 配置 .env.local 中的 QIWEI_AUTH_TOKEN');
  231. console.log(' 3. 双击 qiwei-workbench.exe 启动');
  232. console.log(' 4. 按 README 说明录制参考音频并测试语音发送');
  233. }
  234. }
  235. main().catch(err => {
  236. console.error('测试执行失败:', err.message);
  237. process.exit(1);
  238. });