Ver código fonte

refactor: 升级CosyVoice Instruct为官方格式 - 场景+情感/角色+情感/身份+情感

MyFramework User 2 meses atrás
pai
commit
be4b8b41ca
100 arquivos alterados com 1330 adições e 89 exclusões
  1. 1 1
      AUTO_DEPLOY.md
  2. 105 0
      after-05.yml
  3. 61 0
      after-2x-display.yml
  4. 96 0
      after-speed-change.yml
  5. 1 1
      my-uniapp-vue3/src/components/MiniPlayer.vue
  6. 91 12
      my-uniapp-vue3/src/pages/settings/index.vue
  7. 89 24
      my-uniapp-vue3/src/store/audio.ts
  8. 2 2
      my-uniapp-vue3/src/utils/book-generator-api.ts
  9. 108 0
      picker-open.yml
  10. 96 0
      playing-state.yml
  11. 0 0
      server/logs/requests.json
  12. 2 1
      server/src/config/models.json
  13. 1 1
      server/src/modules/tts/aliyun.provider.ts
  14. 12 7
      server/src/modules/tts/edge-tts.provider.ts
  15. 5 6
      server/src/modules/tts/tts.controller.ts
  16. 255 30
      server/src/modules/tts/tts.service.ts
  17. 23 0
      server/src/types/index.ts
  18. 105 0
      speed-picker.yml
  19. 0 4
      test-results/.last-run.json
  20. BIN
      test-results/.playwright-artifacts-31/page@380cb40bbe1776425196e293c25c7c35.webm
  21. 0 0
      test-results/.playwright-artifacts-31/page@c25e552b876663d7f08a40943363d24d.webm
  22. BIN
      test-results/.playwright-artifacts-31/page@f14a75fc673ee94d9f77fde9aeba608a.webm
  23. 1 0
      test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-69325878d4bb71a155fc.network
  24. 14 0
      test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-69325878d4bb71a155fc.trace
  25. 1 0
      test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-c2236059e45567d9a205-recording1.network
  26. 14 0
      test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-c2236059e45567d9a205-recording1.trace
  27. 1 0
      test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-ecc3f1339592f61f7e2c-recording2.network
  28. 14 0
      test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-ecc3f1339592f61f7e2c-recording2.trace
  29. 19 0
      test-results/.playwright-artifacts-31/traces/resources/69560aaf89c66c2a4ced9fd65e13b65da88e389c.html
  30. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447427495.jpeg
  31. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447427814.jpeg
  32. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447427859.jpeg
  33. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447431511.jpeg
  34. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447431552.jpeg
  35. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@c25e552b876663d7f08a40943363d24d-1781447431996.jpeg
  36. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@c25e552b876663d7f08a40943363d24d-1781447432305.jpeg
  37. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@c25e552b876663d7f08a40943363d24d-1781447432355.jpeg
  38. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447412104.jpeg
  39. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447412438.jpeg
  40. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447412507.jpeg
  41. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447416209.jpeg
  42. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447416243.jpeg
  43. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447418965.jpeg
  44. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447419012.jpeg
  45. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447421772.jpeg
  46. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447421785.jpeg
  47. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447424425.jpeg
  48. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447424436.jpeg
  49. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447426994.jpeg
  50. BIN
      test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447427044.jpeg
  51. BIN
      test-results/.playwright-artifacts-36/page@0d8fc5bfd24584490f1a40fc160f0f55.webm
  52. BIN
      test-results/.playwright-artifacts-36/page@1c6adc9eb988aa2a4b7fb0b0660b1429.webm
  53. 0 0
      test-results/.playwright-artifacts-36/page@1fa19cee2b4eee15cda1c364fbceb98d.webm
  54. BIN
      test-results/.playwright-artifacts-36/page@366cd3f64036e640f7c5caf40de0c114.webm
  55. BIN
      test-results/.playwright-artifacts-36/page@38d6de42543c8796452cf60e0c8928e5.webm
  56. BIN
      test-results/.playwright-artifacts-36/page@449bf494d9e306424c69e104b3f13a80.webm
  57. BIN
      test-results/.playwright-artifacts-36/page@6bc61f2730b3a0b001e38b7de74759f5.webm
  58. BIN
      test-results/.playwright-artifacts-36/page@88275522e516e813f4307dbf1faec8ce.webm
  59. BIN
      test-results/.playwright-artifacts-36/page@d5a2d39b7ee0ad1293c230a506ea8b69.webm
  60. BIN
      test-results/.playwright-artifacts-36/page@e75cac63153e30886cd7e20c0b57e7d7.webm
  61. BIN
      test-results/.playwright-artifacts-36/page@ed17e53a7643634decc5f2abb45ccfbe.webm
  62. BIN
      test-results/.playwright-artifacts-36/page@ef1b7eba78663d2265d2891ecbfdff74.webm
  63. BIN
      test-results/.playwright-artifacts-36/page@efdd6f61ebac54ae6afcfaa86db3158a.webm
  64. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-0b89fa8e0daae3b7238d-recording4.network
  65. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-0b89fa8e0daae3b7238d-recording4.trace
  66. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-12431a3b247fe64b1787-recording3.network
  67. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-12431a3b247fe64b1787-recording3.trace
  68. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-43b5e9ef614a381f0334.network
  69. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-43b5e9ef614a381f0334.trace
  70. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-6b63c33ef0850b90273d-recording5.network
  71. 13 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-6b63c33ef0850b90273d-recording5.trace
  72. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-7526d11532d6e5e4e1e9-recording11.network
  73. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-7526d11532d6e5e4e1e9-recording11.trace
  74. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b080e21dac605b44266a-recording1.network
  75. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b080e21dac605b44266a-recording1.trace
  76. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b1861679fe32ba5633f2-recording6.network
  77. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b1861679fe32ba5633f2-recording6.trace
  78. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-d3e7bede407937130a8d-recording9.network
  79. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-d3e7bede407937130a8d-recording9.trace
  80. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e675d85849828b7ee09b-recording8.network
  81. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e675d85849828b7ee09b-recording8.trace
  82. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e8d262750fea59dfa098-recording12.network
  83. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e8d262750fea59dfa098-recording12.trace
  84. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f2f287e9ec0b5eef3dc4-recording7.network
  85. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f2f287e9ec0b5eef3dc4-recording7.trace
  86. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f85d86480af68b39727b-recording2.network
  87. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f85d86480af68b39727b-recording2.trace
  88. 1 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-ff309a1eab8f4d3ff88d-recording10.network
  89. 14 0
      test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-ff309a1eab8f4d3ff88d-recording10.trace
  90. 19 0
      test-results/.playwright-artifacts-36/traces/resources/69560aaf89c66c2a4ced9fd65e13b65da88e389c.html
  91. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447403998.jpeg
  92. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447404304.jpeg
  93. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447404364.jpeg
  94. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447407932.jpeg
  95. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447407968.jpeg
  96. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447437800.jpeg
  97. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447437825.jpeg
  98. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447437870.jpeg
  99. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447441675.jpeg
  100. BIN
      test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447441741.jpeg

+ 1 - 1
AUTO_DEPLOY.md

@@ -8,7 +8,7 @@
 - SSH 端口: `22622`
 - 用户: `root`
 - 部署目录: `/data/ai/audio_codebuddy`
-- 前端产物目录: `/data/ai/audio/frontend/build/h5`
+- 前端产物目录: `/data/ai/frontend/build/h5` (线上 nginx root 指向)
 - 后端端口: `3100` (PM2 进程名 `server`)
 
 ## 手动部署流程

+ 105 - 0
after-05.yml

@@ -0,0 +1,105 @@
+- generic [ref=e7]:
+  - generic [ref=e8]:
+    - generic [ref=e10]: ⋁
+    - generic [ref=e12]: 小熊过小溪
+    - generic [ref=e13]:
+      - generic [ref=e15]: ⏰
+      - generic [ref=e17]: 📖
+  - generic [ref=e23]:
+    - generic [ref=e25]: 🎵
+    - generic [ref=e26]: 小熊过小溪
+  - generic [ref=e31]:
+    - generic [ref=e34]: "# 小熊过小溪"
+    - generic [ref=e36]: 小熊走啊走,走过绿绿的草地,穿过高高的小树林。
+    - generic [ref=e38]: 忽然,他听到一阵哗哗哗的声音。
+    - generic [ref=e40]: “咦?
+    - generic [ref=e42]: 这是什么声音呀?
+    - generic [ref=e44]: ”
+    - generic [ref=e46]: 小熊走近一看,呀,原来是一条清清的小溪!
+    - generic [ref=e48]: 溪水哗哗地流着,水流轻轻柔柔的,像在唱一首欢快的歌。
+    - generic [ref=e50]: 小熊想走过去,可是低头看看自己的小脚,又看看溪水,心里有点害怕:“水会不会把脚弄湿呀?
+    - generic [ref=e52]: 会不会滑倒呀?
+    - generic [ref=e54]: ”
+    - generic [ref=e56]: 小熊站在溪边想了想,又看了看。
+    - generic [ref=e58]: 咦?
+    - generic [ref=e60]: 溪水里有好多大石头,一块、两块、三块……排列得整整齐齐,像一座小小的石桥。
+    - generic [ref=e62]: “有了!
+    - generic [ref=e64]: 我可以踩着石头过溪呀!
+    - generic [ref=e66]: ”
+    - generic [ref=e68]: 小熊深吸一口气,鼓起勇气,轻轻跳到第一块石头上。
+    - generic [ref=e70]: “一跳!
+    - generic [ref=e72]: ”他稳稳地站住了。
+    - generic [ref=e74]: 溪水在小熊脚边流过,凉凉的,痒痒的。
+    - generic [ref=e76]: 小熊开心极了,又跳到第二块石头上。
+    - generic [ref=e78]: “二跳!
+    - generic [ref=e80]: ”再跳到第三块石头上。
+    - generic [ref=e82]: “三跳!
+    - generic [ref=e84]: ”
+    - generic [ref=e86]: “哈——我成功啦!
+    - generic [ref=e88]: ”
+    - generic [ref=e90]: 小熊站在对岸,开心地摇摇头,甩甩尾巴。
+    - generic [ref=e92]: 他回头看了看小溪,心里美滋滋的:“原来勇敢试一试,就能成功呀!
+    - generic [ref=e94]: ”
+    - generic [ref=e96]: 小熊整理好背包,抬起头,望望远方的山。
+    - generic [ref=e98]: 他相信,翻过那座山,就能找到甜甜的蜂蜜啦!
+    - generic [ref=e100]: 于是,小熊带着快乐的心情,继续往前走。
+  - generic [ref=e101]:
+    - generic [ref=e102]: 0:38
+    - generic [ref=e110]: 1:32
+  - generic [ref=e111]:
+    - generic [ref=e112]:
+      - generic [ref=e114]: 0.5x
+      - generic [ref=e117]: ⬇
+    - generic [ref=e118]:
+      - generic [ref=e120]: ⏮
+      - generic [ref=e122]: ⏸
+      - generic [ref=e124]: ⏭
+    - generic [ref=e125]:
+      - generic [ref=e127]: 🤍
+      - generic [ref=e128]:
+        - generic [ref=e129]: 📤
+        - generic [ref=e130]: 分享得时长
+  - generic [ref=e131]:
+    - generic [ref=e132]:
+      - generic [ref=e133]:
+        - generic [ref=e134]: 播放列表
+        - generic [ref=e135]: 5首
+      - generic [ref=e136]:
+        - generic [ref=e137]: 🔂
+        - generic [ref=e138]: 顺序
+    - generic [ref=e142]:
+      - generic [ref=e143]:
+        - generic [ref=e144]: "1"
+        - generic [ref=e145]:
+          - generic [ref=e146]: 小熊要出门
+          - generic [ref=e147]: 0:01
+      - generic [ref=e148]:
+        - generic [ref=e149]: "2"
+        - generic [ref=e150]:
+          - generic [ref=e151]: 小熊过小溪
+          - generic [ref=e152]: 1:32
+        - generic [ref=e153]: ▶
+      - generic [ref=e154]:
+        - generic [ref=e155]: "3"
+        - generic [ref=e156]:
+          - generic [ref=e157]: 小熊遇蜜蜂
+          - generic [ref=e158]: 1:00
+      - generic [ref=e159]:
+        - generic [ref=e160]: "4"
+        - generic [ref=e161]:
+          - generic [ref=e162]: 小熊想新办法
+          - generic [ref=e163]: 0:45
+      - generic [ref=e164]:
+        - generic [ref=e165]: "5"
+        - generic [ref=e166]:
+          - generic [ref=e167]: 小熊回家啦
+          - generic [ref=e168]: 1:53
+  - generic [ref=e186]:
+    - generic [ref=e187]: 播放速度
+    - generic [ref=e188]:
+      - generic [ref=e190]: 0.5x
+      - generic [ref=e192]: 0.75x
+      - generic [ref=e194]: 1x
+      - generic [ref=e196]: 1.25x
+      - generic [ref=e198]: 1.5x
+      - generic [ref=e200]: 2x

+ 61 - 0
after-2x-display.yml

@@ -0,0 +1,61 @@
+- generic [ref=e7]:
+  - generic [ref=e8]:
+    - generic [ref=e10]: ⋁
+    - generic [ref=e12]: 小熊遇蜜蜂
+    - generic [ref=e13]:
+      - generic [ref=e15]: ⏰
+      - generic [ref=e17]: 📖
+  - generic [ref=e23]:
+    - generic [ref=e25]: 🎵
+    - generic [ref=e26]: 小熊遇蜜蜂
+  - generic [ref=e101]:
+    - generic [ref=e102]: 0:17
+    - generic [ref=e110]: 1:00
+  - generic [ref=e111]:
+    - generic [ref=e112]:
+      - generic [ref=e114]: 2x
+      - generic [ref=e117]: ⬇
+    - generic [ref=e118]:
+      - generic [ref=e120]: ⏮
+      - generic [ref=e122]: ⏸
+      - generic [ref=e124]: ⏭
+    - generic [ref=e125]:
+      - generic [ref=e127]: 🤍
+      - generic [ref=e128]:
+        - generic [ref=e129]: 📤
+        - generic [ref=e130]: 分享得时长
+  - generic [ref=e131]:
+    - generic [ref=e132]:
+      - generic [ref=e133]:
+        - generic [ref=e134]: 播放列表
+        - generic [ref=e135]: 5首
+      - generic [ref=e136]:
+        - generic [ref=e137]: 🔂
+        - generic [ref=e138]: 顺序
+    - generic [ref=e142]:
+      - generic [ref=e143]:
+        - generic [ref=e144]: "1"
+        - generic [ref=e145]:
+          - generic [ref=e146]: 小熊要出门
+          - generic [ref=e147]: 0:01
+      - generic [ref=e148]:
+        - generic [ref=e149]: "2"
+        - generic [ref=e150]:
+          - generic [ref=e151]: 小熊过小溪
+          - generic [ref=e152]: 1:32
+      - generic [ref=e154]:
+        - generic [ref=e155]: "3"
+        - generic [ref=e156]:
+          - generic [ref=e157]: 小熊遇蜜蜂
+          - generic [ref=e158]: 1:00
+        - generic [ref=e201]: ▶
+      - generic [ref=e159]:
+        - generic [ref=e160]: "4"
+        - generic [ref=e161]:
+          - generic [ref=e162]: 小熊想新办法
+          - generic [ref=e163]: 0:45
+      - generic [ref=e164]:
+        - generic [ref=e165]: "5"
+        - generic [ref=e166]:
+          - generic [ref=e167]: 小熊回家啦
+          - generic [ref=e168]: 1:53

+ 96 - 0
after-speed-change.yml

@@ -0,0 +1,96 @@
+- generic [ref=e7]:
+  - generic [ref=e8]:
+    - generic [ref=e10]: ⋁
+    - generic [ref=e12]: 小熊过小溪
+    - generic [ref=e13]:
+      - generic [ref=e15]: ⏰
+      - generic [ref=e17]: 📖
+  - generic [ref=e18]:
+    - generic [ref=e20]: 🎵
+    - generic [ref=e21]: 小熊过小溪
+  - generic [ref=e26]:
+    - generic [ref=e29]: "# 小熊过小溪"
+    - generic [ref=e31]: 小熊走啊走,走过绿绿的草地,穿过高高的小树林。
+    - generic [ref=e33]: 忽然,他听到一阵哗哗哗的声音。
+    - generic [ref=e35]: “咦?
+    - generic [ref=e37]: 这是什么声音呀?
+    - generic [ref=e39]: ”
+    - generic [ref=e41]: 小熊走近一看,呀,原来是一条清清的小溪!
+    - generic [ref=e43]: 溪水哗哗地流着,水流轻轻柔柔的,像在唱一首欢快的歌。
+    - generic [ref=e45]: 小熊想走过去,可是低头看看自己的小脚,又看看溪水,心里有点害怕:“水会不会把脚弄湿呀?
+    - generic [ref=e47]: 会不会滑倒呀?
+    - generic [ref=e49]: ”
+    - generic [ref=e51]: 小熊站在溪边想了想,又看了看。
+    - generic [ref=e53]: 咦?
+    - generic [ref=e55]: 溪水里有好多大石头,一块、两块、三块……排列得整整齐齐,像一座小小的石桥。
+    - generic [ref=e57]: “有了!
+    - generic [ref=e59]: 我可以踩着石头过溪呀!
+    - generic [ref=e61]: ”
+    - generic [ref=e63]: 小熊深吸一口气,鼓起勇气,轻轻跳到第一块石头上。
+    - generic [ref=e65]: “一跳!
+    - generic [ref=e67]: ”他稳稳地站住了。
+    - generic [ref=e69]: 溪水在小熊脚边流过,凉凉的,痒痒的。
+    - generic [ref=e71]: 小熊开心极了,又跳到第二块石头上。
+    - generic [ref=e73]: “二跳!
+    - generic [ref=e75]: ”再跳到第三块石头上。
+    - generic [ref=e77]: “三跳!
+    - generic [ref=e79]: ”
+    - generic [ref=e81]: “哈——我成功啦!
+    - generic [ref=e83]: ”
+    - generic [ref=e85]: 小熊站在对岸,开心地摇摇头,甩甩尾巴。
+    - generic [ref=e87]: 他回头看了看小溪,心里美滋滋的:“原来勇敢试一试,就能成功呀!
+    - generic [ref=e89]: ”
+    - generic [ref=e91]: 小熊整理好背包,抬起头,望望远方的山。
+    - generic [ref=e93]: 他相信,翻过那座山,就能找到甜甜的蜂蜜啦!
+    - generic [ref=e95]: 于是,小熊带着快乐的心情,继续往前走。
+  - generic [ref=e96]:
+    - generic [ref=e97]: 0:36
+    - generic [ref=e104]: 1:32
+  - generic [ref=e105]:
+    - generic [ref=e106]:
+      - generic [ref=e108]: 2x
+      - generic [ref=e111]: ⬇
+    - generic [ref=e112]:
+      - generic [ref=e114]: ⏮
+      - generic [ref=e116]: ⏸
+      - generic [ref=e118]: ⏭
+    - generic [ref=e119]:
+      - generic [ref=e121]: 🤍
+      - generic [ref=e122]:
+        - generic [ref=e123]: 📤
+        - generic [ref=e124]: 分享得时长
+  - generic [ref=e125]:
+    - generic [ref=e126]:
+      - generic [ref=e127]:
+        - generic [ref=e128]: 播放列表
+        - generic [ref=e129]: 5首
+      - generic [ref=e130]:
+        - generic [ref=e131]: 🔂
+        - generic [ref=e132]: 顺序
+    - generic [ref=e136]:
+      - generic [ref=e137]:
+        - generic [ref=e138]: "1"
+        - generic [ref=e139]:
+          - generic [ref=e140]: 小熊要出门
+          - generic [ref=e141]: 0:01
+      - generic [ref=e142]:
+        - generic [ref=e143]: "2"
+        - generic [ref=e144]:
+          - generic [ref=e145]: 小熊过小溪
+          - generic [ref=e146]: 1:32
+        - generic [ref=e147]: ▶
+      - generic [ref=e148]:
+        - generic [ref=e149]: "3"
+        - generic [ref=e150]:
+          - generic [ref=e151]: 小熊遇蜜蜂
+          - generic [ref=e152]: 1:00
+      - generic [ref=e153]:
+        - generic [ref=e154]: "4"
+        - generic [ref=e155]:
+          - generic [ref=e156]: 小熊想新办法
+          - generic [ref=e157]: 0:45
+      - generic [ref=e158]:
+        - generic [ref=e159]: "5"
+        - generic [ref=e160]:
+          - generic [ref=e161]: 小熊回家啦
+          - generic [ref=e162]: 1:53

+ 1 - 1
my-uniapp-vue3/src/components/MiniPlayer.vue

@@ -48,7 +48,7 @@ const showMiniPlayer = computed(() => {
 
 // 封面颜色
 const coverGradient = computed(() => {
-  const voiceId = audioStore.currentAudio?.voiceId || 'cherry';
+  const voiceId = audioStore.currentAudio?.voiceId || 'voice_01';
   return getCoverGradient(voiceId);
 });
 

+ 91 - 12
my-uniapp-vue3/src/pages/settings/index.vue

@@ -133,6 +133,14 @@
     <view v-if="showVoicePicker" class="picker-modal" @click="showVoicePicker = false">
       <view class="picker-content" @click.stop>
         <text class="picker-title">选择默认音色</text>
+        <view class="dimension-legend">
+          <text class="legend-item">🎂年龄</text>
+          <text class="legend-item">🎵音调</text>
+          <text class="legend-item">⏱语速</text>
+          <text class="legend-item">😊情感</text>
+          <text class="legend-item">🔊音色</text>
+          <text class="legend-item">📺用途</text>
+        </view>
         <scroll-view scroll-y class="voice-list">
           <view
             v-for="voice in voiceOptions"
@@ -146,6 +154,9 @@
               <view class="voice-detail">
                 <text class="voice-name">{{ voice.name }}</text>
                 <text class="voice-desc">{{ voice.desc }}</text>
+                <view v-if="voice.tags" class="voice-tags">
+                  <text v-for="(tag, idx) in voice.tags" :key="idx" class="voice-tag">{{ tag }}</text>
+                </view>
               </view>
             </view>
             <text v-if="preferences.defaultVoiceId === voice.id" class="check-icon">✓</text>
@@ -169,18 +180,55 @@ const themeLabels: Record<string, string> = {
 };
 const showThemePicker = ref(false);
 
-// 音色选项
+// 音色选项(22个:10基础 + 12情感变体,与后端 UNIFIED_VOICES 对齐)
+// 含7维度标签:性别、年龄、音调、语速、情感、特点、用途
 const voiceOptions = [
-  { id: 'cherry', name: '芊悦', avatar: '👩', desc: '温柔女声' },
-  { id: 'ethan', name: '晨煦', avatar: '👨', desc: '沉稳男声' },
-  { id: 'serena', name: '苏瑶', avatar: '👩', desc: '知性女声' },
-  { id: 'chelsie', name: '千雪', avatar: '👩', desc: '清甜女声' },
-  { id: 'momo', name: '茉兔', avatar: '👩', desc: '可爱女声' },
-  { id: 'vivian', name: '十三', avatar: '👩', desc: '活泼女声' },
-  { id: 'moon', name: '月白', avatar: '👨', desc: '温润男声' },
-  { id: 'maia', name: '四月', avatar: '👩', desc: '温暖女声' },
-  { id: 'kai', name: '凯', avatar: '👨', desc: '大气男声' },
-  { id: 'nofish', name: '不吃鱼', avatar: '👨', desc: '磁性男声' },
+  // ── 基础音色(默认自然)──
+  { id: 'voice_01', name: '温柔女声·自然', avatar: '👩', desc: '柔和温暖,情感故事',
+    tags: ['青年', '中音', '中速', '温柔', '圆润', '有声书'] },
+  { id: 'voice_02', name: '磁性男声·自然', avatar: '👨', desc: '低沉有力,悬疑推理',
+    tags: ['青年', '低音', '中速', '沉稳', '磁性', '有声书'] },
+  { id: 'voice_03', name: '活泼女声·自然', avatar: '👩', desc: '清新明亮,儿童故事',
+    tags: ['儿童', '高音', '偏快', '活泼', '清脆', '动画'] },
+  { id: 'voice_04', name: '知性女声',       avatar: '👩', desc: '知性稳重,科普知识',
+    tags: ['中年', '中音', '中速', '沉稳', '圆润', '纪录片'] },
+  { id: 'voice_05', name: '阳光男声·自然', avatar: '👨', desc: '阳光活力,校园青春',
+    tags: ['青年', '偏高', '偏快', '开朗', '清脆', '有声书'] },
+  { id: 'voice_06', name: '沧桑男声',       avatar: '👨', desc: '成熟沧桑,历史军事',
+    tags: ['老年', '低音', '偏慢', '严肃', '浑厚', '纪录片'] },
+  { id: 'voice_07', name: '甜美女声',       avatar: '👩', desc: '甜美可爱,爱情都市',
+    tags: ['青年', '偏高', '中速', '开朗', '甜美', '有声书'] },
+  { id: 'voice_08', name: '清朗男声',       avatar: '👨', desc: '清朗干练,职场商战',
+    tags: ['青年', '中音', '中速', '沉稳', '有力', '新闻播报'] },
+  { id: 'voice_09', name: '亲切女声·自然', avatar: '👩', desc: '亲切自然,日常叙事',
+    tags: ['青年', '中音', '中速', '治愈', '圆润', '有声书'] },
+  { id: 'voice_10', name: '稚嫩童声·自然', avatar: '👧', desc: '稚嫩天真,童话寓言',
+    tags: ['儿童', '偏高', '中速', '活泼', '清脆', '动画'] },
+  // ── 情感变体 ──
+  { id: 'voice_01_happy',     name: '温柔女声·欢快', avatar: '👩', desc: '欢快活泼,轻松故事',
+    tags: ['青年', '中音', '偏快', '开朗', '圆润', '有声书', '欢快'] },
+  { id: 'voice_01_surprised', name: '温柔女声·惊奇', avatar: '👩', desc: '惊奇感叹,奇幻冒险',
+    tags: ['青年', '中音', '中速', '活泼', '圆润', '有声书', '惊奇'] },
+  { id: 'voice_02_fearful',   name: '磁性男声·悬疑', avatar: '👨', desc: '低沉恐惧,悬疑惊悚',
+    tags: ['青年', '低音', '偏慢', '严肃', '磁性', '有声书', '悬疑'] },
+  { id: 'voice_02_angry',     name: '磁性男声·激昂', avatar: '👨', desc: '愤怒有力,热血战斗',
+    tags: ['青年', '低音', '偏快', '严肃', '有力', '有声书', '激昂'] },
+  { id: 'voice_03_happy',     name: '活泼女声·开心', avatar: '👩', desc: '天真快乐,童话故事',
+    tags: ['儿童', '高音', '偏快', '开朗', '清脆', '动画', '欢快'] },
+  { id: 'voice_03_surprised', name: '活泼女声·惊奇', avatar: '👩', desc: '惊奇可爱,魔法冒险',
+    tags: ['儿童', '高音', '中速', '活泼', '清脆', '动画', '惊奇'] },
+  { id: 'voice_05_happy',     name: '阳光男声·活力', avatar: '👨', desc: '热情洋溢,青春校园',
+    tags: ['青年', '偏高', '偏快', '开朗', '清脆', '有声书', '活力'] },
+  { id: 'voice_05_angry',     name: '阳光男声·热血', avatar: '👨', desc: '热血激昂,竞技体育',
+    tags: ['青年', '偏高', '偏快', '严肃', '有力', '有声书', '热血'] },
+  { id: 'voice_09_sad',       name: '亲切女声·深情', avatar: '👩', desc: '深情悲伤,抒情文学',
+    tags: ['青年', '中音', '偏慢', '温柔', '圆润', '有声书', '深情'] },
+  { id: 'voice_09_happy',     name: '亲切女声·温暖', avatar: '👩', desc: '温暖治愈,生活记录',
+    tags: ['青年', '中音', '中速', '治愈', '圆润', '有声书', '温暖'] },
+  { id: 'voice_10_happy',     name: '稚嫩童声·开心', avatar: '👧', desc: '开心童趣,儿童故事',
+    tags: ['儿童', '偏高', '偏快', '开朗', '清脆', '动画', '欢快'] },
+  { id: 'voice_10_surprised', name: '稚嫩童声·好奇', avatar: '👧', desc: '好奇惊讶,探索冒险',
+    tags: ['儿童', '偏高', '中速', '活泼', '清脆', '动画', '好奇'] },
 ];
 
 // 用户偏好
@@ -198,7 +246,7 @@ const preferences = ref<Preferences>({
   playSpeed: 1.0,
   quality: 'standard',
   theme: 'light',
-  defaultVoiceId: 'cherry',
+  defaultVoiceId: 'voice_01',
   defaultVolume: 50,
   autoPlayNext: true,
   wifiOnlyDownload: false,
@@ -540,4 +588,35 @@ onMounted(async () => {
   font-size: 24rpx;
   color: #9ca3af;
 }
+
+/* 维度标签 */
+.voice-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6rpx;
+  margin-top: 8rpx;
+}
+
+.voice-tag {
+  font-size: 20rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 8rpx;
+  background: #f0f0ff;
+  color: #6b6bff;
+  border: 1px solid #d0d0ff;
+}
+
+/* 维度图示 */
+.dimension-legend {
+  display: flex;
+  justify-content: space-around;
+  padding: 10rpx 20rpx 16rpx;
+  border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 8rpx;
+}
+
+.legend-item {
+  font-size: 22rpx;
+  color: #9ca3af;
+}
 </style>

+ 89 - 24
my-uniapp-vue3/src/store/audio.ts

@@ -3,6 +3,43 @@ import { ref, computed } from 'vue';
 import { get, post, getFullUrl } from '../utils/request';
 import type { AudioItem, Voice, VoiceParams } from '../types';
 
+// H5 环境:全局拦截 HTMLAudioElement 实例,解决 uni-app InnerAudioContext
+// 的 _audio 引用失效导致 playbackRate 设置无效的问题
+// uni-app 使用 new Audio() 而非 document.createElement('audio'),必须拦截两者
+// #ifdef H5
+const _allAudioElements = new Set<HTMLAudioElement>();
+
+function _trackAudioElement(el: HTMLAudioElement) {
+  _allAudioElements.add(el);
+  const origRemove = el.remove.bind(el);
+  el.remove = function() {
+    _allAudioElements.delete(el);
+    return origRemove();
+  };
+}
+
+// 拦截 document.createElement('audio')
+const _origCreateElement = document.createElement.bind(document);
+(document as any).createElement = function(tag: string, options?: any) {
+  const el = _origCreateElement(tag, options);
+  if (tag.toLowerCase() === 'audio') {
+    _trackAudioElement(el as HTMLAudioElement);
+  }
+  return el;
+};
+
+// 拦截 new Audio() —— uni-app InnerAudioContext 内部使用此方式
+const _OrigAudio = (window as any).Audio;
+if (_OrigAudio) {
+  (window as any).Audio = class extends _OrigAudio {
+    constructor(src?: string) {
+      super(src);
+      _trackAudioElement(this);
+    }
+  };
+}
+// #endif
+
 export const useAudioStore = defineStore('audio', () => {
   // 状态
   const voices = ref<Voice[]>([]);
@@ -101,6 +138,11 @@ export const useAudioStore = defineStore('audio', () => {
 
     audioContext.onCanplay(() => {
       console.log('音频已准备好播放');
+      // 音频源替换后会重置 playbackRate,在 onCanplay 中重新应用
+      if (playRate.value !== 1) {
+        console.log(`[Audio] onCanplay 重新应用 playbackRate=${playRate.value}`);
+        setPlayRate(playRate.value);
+      }
     });
   }
 
@@ -224,23 +266,16 @@ export const useAudioStore = defineStore('audio', () => {
     if (ctx.src === fullUrl) {
       console.log('音频源相同,切换播放状态');
       ctx.play();
+      // 同源恢复播放时确保速度设置(onCanplay 不会再次触发)
+      if (playRate.value !== 1) {
+        setPlayRate(playRate.value);
+      }
     } else {
       console.log('设置新音频源并播放:', fullUrl);
       ctx.src = fullUrl;
       ctx.play();
-    }
-
-    // 重新应用 playRate(切歌后 audioContext 会重置 playbackRate)
-    if (!useBackgroundAudio && audioContext) {
-      try {
-        audioContext.playbackRate = playRate.value;
-        console.log(`[Audio] play() 后重新应用 playbackRate=${playRate.value}`);
-        // H5 兼容
-        // #ifdef H5
-        const innerAny = audioContext as any;
-        if (innerAny._audio) innerAny._audio.playbackRate = playRate.value;
-        // #endif
-      } catch (e) {}
+      // 换源后 playbackRate 会重置,onCanplay 会自动重新应用 playRate
+      // 这里不做重复设置,避免竞态
     }
 
     // 使用 API 返回的 audioDuration 作为初始时长
@@ -355,31 +390,61 @@ export const useAudioStore = defineStore('audio', () => {
     playRate.value = rate;
     console.log(`[Audio] setPlayRate=${rate}, useBackgroundAudio=${useBackgroundAudio}, hasBg=${!!bgAudioManager}, hasCtx=${!!audioContext}`);
 
-    // BackgroundAudioManager:尝试设置(部分平台支持,失败不影响)
+    // BackgroundAudioManager:App 平台不支持 playbackRate
     if (useBackgroundAudio && bgAudioManager) {
-      try {
-        // #ifdef APP-PLUS
-        // App 平台 BackgroundAudioManager 不支持 playbackRate
+      // #ifdef APP-PLUS
+      return;
+      // #endif
+    }
+
+    // H5:优先通过全局拦截的 audio 元素集合设置(最可靠)
+    // #ifdef H5
+    try {
+      const audioElements = document.querySelectorAll('audio');
+      
+      // 先尝试 DOM 中的 audio 元素
+      if (audioElements.length > 0) {
+        audioElements.forEach((el: HTMLAudioElement) => {
+          el.playbackRate = rate;
+          console.log(`[Audio] DOM audio.playbackRate=${rate}, 实际=${el.playbackRate}`);
+        });
         return;
-        // #endif
-      } catch (e) {}
+      }
+      
+      // 再尝试全局拦截集合(uni-app H5 创建的 audio 不在 DOM 中)
+      if (_allAudioElements.size > 0) {
+        let applied = 0;
+        _allAudioElements.forEach((el) => {
+          try {
+            el.playbackRate = rate;
+            applied++;
+            console.log(`[Audio] GlobalSet audio.playbackRate=${rate}, 实际=${el.playbackRate}, readyState=${el.readyState}, paused=${el.paused}`);
+          } catch (e) {}
+        });
+        console.log(`[Audio] GlobalSet: ${applied}/${_allAudioElements.size} 个元素已设置 playbackRate=${rate}`);
+      } else {
+        console.warn('[Audio] H5: DOM 和 GlobalSet 均无 audio 元素,降级使用 uni API');
+      }
+    } catch (e) {
+      console.error('[Audio] H5 设置 playbackRate 异常:', e);
     }
+    // #endif
 
-    // H5/InnerAudioContext 模式:设置 playbackRate
+    // 降级:uni-app InnerAudioContext API
     if (audioContext) {
       try {
         audioContext.playbackRate = rate;
-        console.log(`[Audio] playbackRate=${rate} 设置成功`);
+        console.log(`[Audio] uni-api playbackRate=${rate} 设置成功`);
       } catch (e) {
-        console.error('[Audio] 设置 playbackRate 失败:', e);
+        console.error('[Audio] uni-api 设置 playbackRate 失败:', e);
       }
-      // H5 兼容:直接操作底层 audio 元素(解决 uniapp H5 端 playbackRate 不生效的问题)
+      // H5 兼容:直接操作底层 audio 元素
       // #ifdef H5
       try {
         const innerAny = audioContext as any;
         if (innerAny._audio && innerAny._audio.playbackRate !== rate) {
           innerAny._audio.playbackRate = rate;
-          console.log(`[Audio] H5 底层 audio.playbackRate=${rate} 设置成功`);
+          console.log(`[Audio] H5 _audio.playbackRate=${rate} 设置成功`);
         }
       } catch (e) {}
       // #endif

+ 2 - 2
my-uniapp-vue3/src/utils/book-generator-api.ts

@@ -400,7 +400,7 @@ export async function generateWithLangGraph(bookId: string): Promise<{ bookId: s
 export async function generateChapterAudio(
   bookId: string,
   chapterNumber: number,
-  voiceId: string = 'cherry'
+  voiceId: string = 'voice_01'
 ): Promise<{ taskId: string; totalSubsections: number }> {
   // 后端只有批量生成API,所以调用批量生成
   const result = await request<{ taskId: string; totalSubsections: number }>(
@@ -418,7 +418,7 @@ export async function generateChapterAudio(
  */
 export async function generateAllChaptersAudio(
   bookId: string,
-  voiceId: string = 'cherry'
+  voiceId: string = 'voice_01'
 ): Promise<{ taskId: string; totalChapters: number }> {
   const result = await request<{ taskId: string; totalSubsections: number }>(
     `${BASE_URL}/books/${bookId}/audio`,

+ 108 - 0
picker-open.yml

@@ -0,0 +1,108 @@
+- generic [ref=e7]:
+  - generic [ref=e8]:
+    - generic [ref=e10]: ⋁
+    - generic [ref=e12]: 小熊过小溪
+    - generic [ref=e13]:
+      - generic [ref=e15]: ⏰
+      - generic [ref=e17]: 📖
+  - generic [ref=e18]:
+    - generic [ref=e20]: 🎵
+    - generic [ref=e21]: 小熊过小溪
+  - generic [ref=e26]:
+    - generic [ref=e29]: "# 小熊过小溪"
+    - generic [ref=e31]: 小熊走啊走,走过绿绿的草地,穿过高高的小树林。
+    - generic [ref=e33]: 忽然,他听到一阵哗哗哗的声音。
+    - generic [ref=e35]: “咦?
+    - generic [ref=e37]: 这是什么声音呀?
+    - generic [ref=e39]: ”
+    - generic [ref=e41]: 小熊走近一看,呀,原来是一条清清的小溪!
+    - generic [ref=e43]: 溪水哗哗地流着,水流轻轻柔柔的,像在唱一首欢快的歌。
+    - generic [ref=e45]: 小熊想走过去,可是低头看看自己的小脚,又看看溪水,心里有点害怕:“水会不会把脚弄湿呀?
+    - generic [ref=e47]: 会不会滑倒呀?
+    - generic [ref=e49]: ”
+    - generic [ref=e51]: 小熊站在溪边想了想,又看了看。
+    - generic [ref=e53]: 咦?
+    - generic [ref=e55]: 溪水里有好多大石头,一块、两块、三块……排列得整整齐齐,像一座小小的石桥。
+    - generic [ref=e57]: “有了!
+    - generic [ref=e59]: 我可以踩着石头过溪呀!
+    - generic [ref=e61]: ”
+    - generic [ref=e63]: 小熊深吸一口气,鼓起勇气,轻轻跳到第一块石头上。
+    - generic [ref=e65]: “一跳!
+    - generic [ref=e67]: ”他稳稳地站住了。
+    - generic [ref=e69]: 溪水在小熊脚边流过,凉凉的,痒痒的。
+    - generic [ref=e71]: 小熊开心极了,又跳到第二块石头上。
+    - generic [ref=e73]: “二跳!
+    - generic [ref=e75]: ”再跳到第三块石头上。
+    - generic [ref=e77]: “三跳!
+    - generic [ref=e79]: ”
+    - generic [ref=e81]: “哈——我成功啦!
+    - generic [ref=e83]: ”
+    - generic [ref=e85]: 小熊站在对岸,开心地摇摇头,甩甩尾巴。
+    - generic [ref=e87]: 他回头看了看小溪,心里美滋滋的:“原来勇敢试一试,就能成功呀!
+    - generic [ref=e89]: ”
+    - generic [ref=e91]: 小熊整理好背包,抬起头,望望远方的山。
+    - generic [ref=e93]: 他相信,翻过那座山,就能找到甜甜的蜂蜜啦!
+    - generic [ref=e95]: 于是,小熊带着快乐的心情,继续往前走。
+  - generic [ref=e96]:
+    - generic [ref=e97]: 0:09
+    - generic [ref=e104]: 1:32
+  - generic [ref=e105]:
+    - generic [ref=e106]:
+      - generic [ref=e108]: 1x
+      - generic [ref=e111]: ⬇
+    - generic [ref=e112]:
+      - generic [ref=e114]: ⏮
+      - generic [ref=e116]: ⏸
+      - generic [ref=e118]: ⏭
+    - generic [ref=e119]:
+      - generic [ref=e121]: 🤍
+      - generic [ref=e122]:
+        - generic [ref=e123]: 📤
+        - generic [ref=e124]: 分享得时长
+  - generic [ref=e125]:
+    - generic [ref=e126]:
+      - generic [ref=e127]:
+        - generic [ref=e128]: 播放列表
+        - generic [ref=e129]: 5首
+      - generic [ref=e130]:
+        - generic [ref=e131]: 🔂
+        - generic [ref=e132]: 顺序
+    - generic [ref=e133]:
+      - generic:
+        - generic:
+          - generic:
+            - generic [ref=e134]:
+              - generic [ref=e135]: "1"
+              - generic [ref=e136]:
+                - generic [ref=e137]: 小熊要出门
+                - generic [ref=e138]: 0:01
+            - generic [ref=e139]:
+              - generic [ref=e140]: "2"
+              - generic [ref=e141]:
+                - generic [ref=e142]: 小熊过小溪
+                - generic [ref=e143]: 1:32
+              - generic [ref=e144]: ▶
+            - generic [ref=e145]:
+              - generic [ref=e146]: "3"
+              - generic [ref=e147]:
+                - generic [ref=e148]: 小熊遇蜜蜂
+                - generic [ref=e149]: 1:00
+            - generic [ref=e150]:
+              - generic [ref=e151]: "4"
+              - generic [ref=e152]:
+                - generic [ref=e153]: 小熊想新办法
+                - generic [ref=e154]: 0:45
+            - generic [ref=e155]:
+              - generic [ref=e156]: "5"
+              - generic [ref=e157]:
+                - generic [ref=e158]: 小熊回家啦
+                - generic [ref=e159]: 1:53
+  - generic [ref=e162]:
+    - generic [ref=e163]: 播放速度
+    - generic [ref=e164]:
+      - generic [ref=e166]: 0.5x
+      - generic [ref=e168]: 0.75x
+      - generic [ref=e170]: 1x
+      - generic [ref=e172]: 1.25x
+      - generic [ref=e174]: 1.5x
+      - generic [ref=e176]: 2x

+ 96 - 0
playing-state.yml

@@ -0,0 +1,96 @@
+- generic [ref=e7]:
+  - generic [ref=e8]:
+    - generic [ref=e10]: ⋁
+    - generic [ref=e12]: 小熊过小溪
+    - generic [ref=e13]:
+      - generic [ref=e15]: ⏰
+      - generic [ref=e17]: 📖
+  - generic [ref=e18]:
+    - generic [ref=e20]: 🎵
+    - generic [ref=e21]: 小熊过小溪
+  - generic [ref=e26]:
+    - generic [ref=e29]: "# 小熊过小溪"
+    - generic [ref=e31]: 小熊走啊走,走过绿绿的草地,穿过高高的小树林。
+    - generic [ref=e33]: 忽然,他听到一阵哗哗哗的声音。
+    - generic [ref=e35]: “咦?
+    - generic [ref=e37]: 这是什么声音呀?
+    - generic [ref=e39]: ”
+    - generic [ref=e41]: 小熊走近一看,呀,原来是一条清清的小溪!
+    - generic [ref=e43]: 溪水哗哗地流着,水流轻轻柔柔的,像在唱一首欢快的歌。
+    - generic [ref=e45]: 小熊想走过去,可是低头看看自己的小脚,又看看溪水,心里有点害怕:“水会不会把脚弄湿呀?
+    - generic [ref=e47]: 会不会滑倒呀?
+    - generic [ref=e49]: ”
+    - generic [ref=e51]: 小熊站在溪边想了想,又看了看。
+    - generic [ref=e53]: 咦?
+    - generic [ref=e55]: 溪水里有好多大石头,一块、两块、三块……排列得整整齐齐,像一座小小的石桥。
+    - generic [ref=e57]: “有了!
+    - generic [ref=e59]: 我可以踩着石头过溪呀!
+    - generic [ref=e61]: ”
+    - generic [ref=e63]: 小熊深吸一口气,鼓起勇气,轻轻跳到第一块石头上。
+    - generic [ref=e65]: “一跳!
+    - generic [ref=e67]: ”他稳稳地站住了。
+    - generic [ref=e69]: 溪水在小熊脚边流过,凉凉的,痒痒的。
+    - generic [ref=e71]: 小熊开心极了,又跳到第二块石头上。
+    - generic [ref=e73]: “二跳!
+    - generic [ref=e75]: ”再跳到第三块石头上。
+    - generic [ref=e77]: “三跳!
+    - generic [ref=e79]: ”
+    - generic [ref=e81]: “哈——我成功啦!
+    - generic [ref=e83]: ”
+    - generic [ref=e85]: 小熊站在对岸,开心地摇摇头,甩甩尾巴。
+    - generic [ref=e87]: 他回头看了看小溪,心里美滋滋的:“原来勇敢试一试,就能成功呀!
+    - generic [ref=e89]: ”
+    - generic [ref=e91]: 小熊整理好背包,抬起头,望望远方的山。
+    - generic [ref=e93]: 他相信,翻过那座山,就能找到甜甜的蜂蜜啦!
+    - generic [ref=e95]: 于是,小熊带着快乐的心情,继续往前走。
+  - generic [ref=e96]:
+    - generic [ref=e97]: 0:04
+    - generic [ref=e104]: 1:32
+  - generic [ref=e105]:
+    - generic [ref=e106]:
+      - generic [ref=e108]: 1x
+      - generic [ref=e111]: ⬇
+    - generic [ref=e112]:
+      - generic [ref=e114]: ⏮
+      - generic [ref=e116]: ⏸
+      - generic [ref=e118]: ⏭
+    - generic [ref=e119]:
+      - generic [ref=e121]: 🤍
+      - generic [ref=e122]:
+        - generic [ref=e123]: 📤
+        - generic [ref=e124]: 分享得时长
+  - generic [ref=e125]:
+    - generic [ref=e126]:
+      - generic [ref=e127]:
+        - generic [ref=e128]: 播放列表
+        - generic [ref=e129]: 5首
+      - generic [ref=e130]:
+        - generic [ref=e131]: 🔂
+        - generic [ref=e132]: 顺序
+    - generic [ref=e136]:
+      - generic [ref=e137]:
+        - generic [ref=e138]: "1"
+        - generic [ref=e139]:
+          - generic [ref=e140]: 小熊要出门
+          - generic [ref=e141]: 0:01
+      - generic [ref=e142]:
+        - generic [ref=e143]: "2"
+        - generic [ref=e144]:
+          - generic [ref=e145]: 小熊过小溪
+          - generic [ref=e146]: 1:32
+        - generic [ref=e147]: ▶
+      - generic [ref=e148]:
+        - generic [ref=e149]: "3"
+        - generic [ref=e150]:
+          - generic [ref=e151]: 小熊遇蜜蜂
+          - generic [ref=e152]: 1:00
+      - generic [ref=e153]:
+        - generic [ref=e154]: "4"
+        - generic [ref=e155]:
+          - generic [ref=e156]: 小熊想新办法
+          - generic [ref=e157]: 0:45
+      - generic [ref=e158]:
+        - generic [ref=e159]: "5"
+        - generic [ref=e160]:
+          - generic [ref=e161]: 小熊回家啦
+          - generic [ref=e162]: 1:53

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
server/logs/requests.json


+ 2 - 1
server/src/config/models.json

@@ -29,7 +29,8 @@
     "bailian": {
       "name": "阿里云百炼",
       "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
-      "ttsApiPath": "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
+      "ttsApiPath": "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer",
+      "_ttsApiPathComment": "实际 endpoint 在 aliyun.provider.ts 中按模型动态选择 (cosyvoice 用 audio/tts/SpeechSynthesizer, qwen 用 aigc/multimodal-generation/generation)",
       "apiKey": "sk-c25679401ba24c749f53be86b0c9a7a6",
       "apiType": "openai-chat",
       "priority": 3,

+ 1 - 1
server/src/modules/tts/aliyun.provider.ts

@@ -89,7 +89,7 @@ export class AliyunTtsProvider implements ITtsProvider {
         if (isCosyVoice) {
           requestBody.input.format = 'mp3';
           requestBody.input.sample_rate = 48000;
-          // CosyVoice v3-flash 系统音色支持 instruction 情感控制(固定格式 设置情感:{emotion}。)
+          // CosyVoice v3-flash 系统音色支持 Instruct 情感控制(官方格式:你说话的情感是<值>。)
           if ((params as any).instructText) {
             requestBody.input.instruction = (params as any).instructText;
           }

+ 12 - 7
server/src/modules/tts/edge-tts.provider.ts

@@ -98,22 +98,26 @@ export class EdgeTtsProvider implements ITtsProvider {
     ];
 
     // 语速映射:speed 0.5~2.0 → rate -50%~+100%
+    // 注意:使用 --rate=<value> 格式而非 --rate <value>,避免 shell 解析负值参数
     if (params.speed !== undefined && params.speed !== 1) {
       const ratePercent = Math.round((params.speed - 1) * 100);
-      args.push('--rate', `${ratePercent > 0 ? '+' : ''}${ratePercent}%`);
+      const sign = ratePercent > 0 ? '+' : '';
+      args.push(`--rate=${sign}${ratePercent}%`);
     }
 
     // 音调映射:pitch -500~500 → pitch -50Hz~+50Hz
     if (params.pitch !== undefined && params.pitch !== 0) {
       const pitchHz = Math.round(params.pitch / 10);
-      args.push('--pitch', `${pitchHz > 0 ? '+' : ''}${pitchHz}Hz`);
+      const sign = pitchHz > 0 ? '+' : '';
+      args.push(`--pitch=${sign}${pitchHz}Hz`);
     }
 
     // 音量映射:volume 0~100 → -100%~+100%(默认50不调整)
     if (params.volume !== undefined && params.volume !== 50) {
       const volPercent = Math.round((params.volume / 50 - 1) * 100);
       if (volPercent !== 0) {
-        args.push('--volume', `${volPercent > 0 ? '+' : ''}${volPercent}%`);
+        const sign = volPercent > 0 ? '+' : '';
+        args.push(`--volume=${sign}${volPercent}%`);
       }
     }
 
@@ -122,9 +126,10 @@ export class EdgeTtsProvider implements ITtsProvider {
     const startTime = Date.now();
 
     return new Promise<string>((resolve, reject) => {
-      // 优先用 edge-tts 命令(避免 python -m 解析长文本的 argparse 问题),fallback 到 python -m
+      // 优先用 edge-tts 命令(避免 python -m 解析长文本的 argparse 问题)
+      // shell: false 避免 shell 解析带负号的参数(如 -20%)
       const edgeCmd = spawn('edge-tts', args, {
-        shell: true,
+        shell: false,
         stdio: ['pipe', 'pipe', 'pipe'],
       });
 
@@ -174,7 +179,7 @@ export class EdgeTtsProvider implements ITtsProvider {
     reject: (error: Error) => void,
   ) {
     const child = spawn('edge-tts', args, {
-      shell: true,
+      shell: false,
       stdio: ['pipe', 'pipe', 'pipe'],
     });
 
@@ -206,7 +211,7 @@ export class EdgeTtsProvider implements ITtsProvider {
   async healthCheck(): Promise<boolean> {
     return new Promise((resolve) => {
       const child = spawn('edge-tts', ['--list-voices'], {
-        shell: true,
+        shell: false,
         stdio: 'pipe',
       });
 

+ 5 - 6
server/src/modules/tts/tts.controller.ts

@@ -48,14 +48,14 @@ router.post('/detect-emotion', async (ctx: Context) => {
   const detected = TtsService.detectBestVoiceAndEmotion(text);
   if (!detected) {
     // 兜底:至少返回情感检测结果
-    const fallbackEmotion = TtsService.getVoiceInstruct(text).replace('设置情感:', '').replace('。', '');
+    const emotion = TtsService.detectEmotion(text);
     ctx.body = {
       code: 0,
       message: 'success',
       data: {
         detected: false,
-        emotion: fallbackEmotion || 'neutral',
-        emotionLabel: TtsService.EMOTION_LABELS[fallbackEmotion] || '中性',
+        emotion,
+        emotionLabel: TtsService.EMOTION_LABELS[emotion] || '中性',
         hint: '未匹配到特定内容类型,使用通用情感',
       },
     };
@@ -63,7 +63,6 @@ router.post('/detect-emotion', async (ctx: Context) => {
   }
 
   const voiceInfo = TtsService.getVoiceById(detected.voiceId);
-  const emotion = detected.instructText.replace('设置情感:', '').replace('。', '');
 
   ctx.body = {
     code: 0,
@@ -72,8 +71,8 @@ router.post('/detect-emotion', async (ctx: Context) => {
       detected: true,
       voiceId: detected.voiceId,
       voiceName: voiceInfo?.name || detected.voiceId,
-      emotion,
-      emotionLabel: TtsService.EMOTION_LABELS[emotion] || emotion,
+      emotion: detected.cosyEmotion,
+      emotionLabel: TtsService.EMOTION_LABELS[detected.cosyEmotion] || detected.cosyEmotion,
       instructText: detected.instructText,
     },
   };

+ 255 - 30
server/src/modules/tts/tts.service.ts

@@ -14,19 +14,186 @@ import { ProviderNode } from '../../common/provider-registry';
 import { ttsLogger } from './tts-logger';
 import axios from 'axios';
 
-// ============ 统一音色定义(10个固定音色,前端使用)============
-// 前端使用统一 ID,后端根据 Provider 类型映射到真实音色
+// ============ CosyVoice 官方 Instruct 指令生成 ============
+// 严格按官方格式,使用中文,包括标点符号,不可遗漏结尾句号。
+// 支持的情感值:neutral、fearful、angry、sad、surprised、happy、disgusted
+// 优先级:身份 > 角色 > 场景 > 仅情感
+
+/** 统一的情感值校验:只返回官方支持的7种情感之一,非法值降级为 neutral */
+function normalizeEmotion(emotion?: string): string {
+  const valid = ['neutral', 'fearful', 'angry', 'sad', 'surprised', 'happy', 'disgusted'];
+  return emotion && valid.includes(emotion) ? emotion : 'neutral';
+}
+
+/** 生成 CosyVoice 格式的最终 Instruct 指令文本 */
+function generateInstruct(voice: Voice): string | undefined {
+  if (!voice.cosyEmotion && !voice.scene && !voice.role && !voice.identity) {
+    return undefined;
+  }
+  const emotion = normalizeEmotion(voice.cosyEmotion);
+
+  if (voice.identity) {
+    return `你正在以一个${voice.identity}的身份说话,你说话的情感是${emotion}。`;
+  }
+  if (voice.role) {
+    return `你现在说话的角色是${voice.role},你说话的情感是${emotion}。`;
+  }
+  if (voice.scene) {
+    return `你正在进行${voice.scene},你说话的情感是${emotion}。`;
+  }
+  return `你说话的情感是${emotion}。`;
+}
+
+/** 仅情感指令(无场景/角色/身份时使用) */
+function emotionOnlyInstruct(emotion: string): string {
+  return `你说话的情感是${normalizeEmotion(emotion)}。`;
+}
+
+// ============ 统一音色定义(22个音色,10基础 + 12个情感变体)============
+// 6个支持 Instruct 的基础音色各扩展 2 个情感变体 → 10 + 12 = 22
+// 每条音色包含 7 个维度描述 + 官方 Instruct 场景,instructText 由 generateInstruct 自动生成
 const UNIFIED_VOICES: Voice[] = [
-  { id: 'voice_01', name: '温柔女声',  gender: 'female', description: '柔和温暖,适合情感故事' },
-  { id: 'voice_02', name: '磁性男声',  gender: 'male',   description: '低沉有力,适合悬疑推理' },
-  { id: 'voice_03', name: '活泼女声',  gender: 'female', description: '清新明亮,适合儿童故事' },
-  { id: 'voice_04', name: '知性女声',  gender: 'female', description: '知性稳重,适合科普知识' },
-  { id: 'voice_05', name: '阳光男声',  gender: 'male',   description: '阳光活力,适合校园青春' },
-  { id: 'voice_06', name: '沧桑男声',  gender: 'male',   description: '成熟沧桑,适合历史军事' },
-  { id: 'voice_07', name: '甜美女声',  gender: 'female', description: '甜美可爱,适合爱情都市' },
-  { id: 'voice_08', name: '清朗男声',  gender: 'male',   description: '清朗干练,适合职场商战' },
-  { id: 'voice_09', name: '亲切女声',  gender: 'female', description: '亲切自然,适合日常叙事' },
-  { id: 'voice_10', name: '稚嫩童声',  gender: 'female', description: '稚嫩天真,适合童话寓言' },
+  // ═══════════════════ 基础音色(默认自然风格)═══════════════════
+  {
+    id: 'voice_01', name: '温柔女声·自然', gender: 'female',
+    description: '柔和温暖,适合情感故事',
+    age: '青年', pitch: '中音', speed: '中速', style: '温柔',
+    characteristics: '圆润甜美', usage: '有声书', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_02', name: '磁性男声·自然', gender: 'male',
+    description: '低沉有力,适合悬疑推理',
+    age: '青年', pitch: '低音', speed: '中速', style: '沉稳',
+    characteristics: '有磁性浑厚', usage: '有声书', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_03', name: '活泼女声·自然', gender: 'female',
+    description: '清新明亮,适合儿童故事',
+    age: '儿童', pitch: '高音', speed: '偏快', style: '活泼',
+    characteristics: '清脆甜美', usage: '动画角色', scene: '一些儿童内容解说',
+  },
+  {
+    id: 'voice_04', name: '知性女声', gender: 'female',
+    description: '知性稳重,适合科普知识(不支持情感变体)',
+    age: '中年', pitch: '中音', speed: '中速', style: '沉稳',
+    characteristics: '圆润', usage: '纪录片解说', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_05', name: '阳光男声·自然', gender: 'male',
+    description: '阳光活力,适合校园青春',
+    age: '青年', pitch: '偏高', speed: '偏快', style: '开朗',
+    characteristics: '清脆有力', usage: '有声书', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_06', name: '沧桑男声', gender: 'male',
+    description: '成熟沧桑,适合历史军事(不支持情感变体)',
+    age: '老年', pitch: '低音', speed: '偏慢', style: '严肃',
+    characteristics: '沙哑浑厚', usage: '纪录片解说', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_07', name: '甜美女声', gender: 'female',
+    description: '甜美可爱,适合爱情都市(不支持情感变体)',
+    age: '青年', pitch: '偏高', speed: '中速', style: '开朗',
+    characteristics: '甜美圆润', usage: '有声书', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_08', name: '清朗男声', gender: 'male',
+    description: '清朗干练,适合职场商战(不支持情感变体)',
+    age: '青年', pitch: '中音', speed: '中速', style: '沉稳',
+    characteristics: '清脆有力', usage: '新闻播报', scene: '新闻播报',
+  },
+  {
+    id: 'voice_09', name: '亲切女声·自然', gender: 'female',
+    description: '亲切自然,适合日常叙事',
+    age: '青年', pitch: '中音', speed: '中速', style: '治愈',
+    characteristics: '圆润甜美', usage: '有声书', scene: '闲聊互动',
+  },
+  {
+    id: 'voice_10', name: '稚嫩童声·自然', gender: 'female',
+    description: '稚嫩天真,适合童话寓言',
+    age: '儿童', pitch: '偏高', speed: '中速', style: '活泼',
+    characteristics: '清脆甜美', usage: '动画角色', scene: '一些儿童内容解说',
+  },
+
+  // ═══════════════════ 情感变体 ════════════════════
+  // voice_01 → longanhuan_v3 (元气女) → +happy, +surprised
+  {
+    id: 'voice_01_happy', name: '温柔女声·欢快', gender: 'female',
+    description: '欢快活泼,适合轻松故事',
+    age: '青年', pitch: '中音', speed: '偏快', style: '开朗',
+    characteristics: '圆润甜美', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'happy',
+  },
+  {
+    id: 'voice_01_surprised', name: '温柔女声·惊奇', gender: 'female',
+    description: '惊奇感叹,适合奇幻冒险',
+    age: '青年', pitch: '中音', speed: '中速', style: '活泼',
+    characteristics: '圆润甜美', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'surprised',
+  },
+  // voice_02 → longanyang (磁性男) → +fearful, +angry
+  {
+    id: 'voice_02_fearful', name: '磁性男声·悬疑', gender: 'male',
+    description: '低沉恐惧,适合悬疑惊悚',
+    age: '青年', pitch: '低音', speed: '偏慢', style: '严肃',
+    characteristics: '有磁性沙哑', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'fearful',
+  },
+  {
+    id: 'voice_02_angry', name: '磁性男声·激昂', gender: 'male',
+    description: '愤怒有力,适合热血战斗',
+    age: '青年', pitch: '低音', speed: '偏快', style: '严肃',
+    characteristics: '有磁性有力', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'angry',
+  },
+  // voice_03 → longhuhu_v3 (女童音) → +happy, +surprised
+  {
+    id: 'voice_03_happy', name: '活泼女声·开心', gender: 'female',
+    description: '天真快乐,适合童话故事',
+    age: '儿童', pitch: '高音', speed: '偏快', style: '开朗',
+    characteristics: '清脆甜美', usage: '动画角色', scene: '一些儿童内容解说', cosyEmotion: 'happy',
+  },
+  {
+    id: 'voice_03_surprised', name: '活泼女声·惊奇', gender: 'female',
+    description: '惊奇可爱,适合魔法冒险',
+    age: '儿童', pitch: '高音', speed: '中速', style: '活泼',
+    characteristics: '清脆甜美', usage: '动画角色', scene: '一些儿童内容解说', cosyEmotion: 'surprised',
+  },
+  // voice_05 → longyichen_v3 (阳光男) → +happy, +angry
+  {
+    id: 'voice_05_happy', name: '阳光男声·活力', gender: 'male',
+    description: '热情洋溢,适合青春校园',
+    age: '青年', pitch: '偏高', speed: '偏快', style: '开朗',
+    characteristics: '清脆有力', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'happy',
+  },
+  {
+    id: 'voice_05_angry', name: '阳光男声·热血', gender: 'male',
+    description: '热血激昂,适合竞技体育',
+    age: '青年', pitch: '偏高', speed: '偏快', style: '严肃',
+    characteristics: '清脆有力', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'angry',
+  },
+  // voice_09 → longanrou_v3 (温柔闺蜜) → +sad, +happy
+  {
+    id: 'voice_09_sad', name: '亲切女声·深情', gender: 'female',
+    description: '深情悲伤,适合抒情文学',
+    age: '青年', pitch: '中音', speed: '偏慢', style: '温柔',
+    characteristics: '圆润甜美', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'sad',
+  },
+  {
+    id: 'voice_09_happy', name: '亲切女声·温暖', gender: 'female',
+    description: '温暖治愈,适合生活记录',
+    age: '青年', pitch: '中音', speed: '中速', style: '治愈',
+    characteristics: '圆润甜美', usage: '有声书', scene: '闲聊互动', cosyEmotion: 'happy',
+  },
+  // voice_10 → longpaopao_v3 (童声泡泡音) → +happy, +surprised
+  {
+    id: 'voice_10_happy', name: '稚嫩童声·开心', gender: 'female',
+    description: '开心童趣,适合儿童故事',
+    age: '儿童', pitch: '偏高', speed: '偏快', style: '开朗',
+    characteristics: '清脆甜美', usage: '动画角色', scene: '一些儿童内容解说', cosyEmotion: 'happy',
+  },
+  {
+    id: 'voice_10_surprised', name: '稚嫩童声·好奇', gender: 'female',
+    description: '好奇惊讶,适合探索冒险',
+    age: '儿童', pitch: '偏高', speed: '中速', style: '活泼',
+    characteristics: '清脆甜美', usage: '动画角色', scene: '一些儿童内容解说', cosyEmotion: 'surprised',
+  },
 ];
 
 // 统一音色 → 阿里云 CosyVoice 真实音色映射
@@ -58,16 +225,33 @@ const EDGE_VOICE_MAP: Record<string, string> = {
   voice_10: 'zh-CN-XiaoshuangNeural',  // 稚嫩童声 → 晓双(童声)
 };
 
+/** 从变体 ID 提取基础音色 ID(voice_01_happy → voice_01, voice_01 → voice_01) */
+function getBaseVoiceId(voiceId: string): string {
+  const match = voiceId.match(/^(voice_\d+)/);
+  return match ? match[1] : voiceId;
+}
+
+/** 根据音色 ID 获取官方的 instructText(优先显式值,否则从维度自动生成) */
+function getVoiceInstructById(voiceId: string): string {
+  const voice = UNIFIED_VOICES.find(v => v.id === voiceId);
+  if (!voice) return '';
+  // 显式 instructText 优先(向后兼容),否则从维度自动生成
+  if (voice.instructText) return voice.instructText;
+  return generateInstruct(voice) || '';
+}
+
 function mapToProviderVoice(unifiedVoiceId: string, providerVendor: string): string {
+  const baseId = getBaseVoiceId(unifiedVoiceId);
+
   // Edge-TTS: 使用微软免费音色
   if (providerVendor === 'edge') {
-    const mapped = EDGE_VOICE_MAP[unifiedVoiceId];
+    const mapped = EDGE_VOICE_MAP[baseId];
     if (mapped) return mapped;
     console.warn(`⚠️ [VoiceMap] Edge 未识别的音色ID: "${unifiedVoiceId}",降级使用默认音色`);
     return 'zh-CN-XiaoxiaoNeural';
   }
   // 默认: 阿里云百炼 CosyVoice
-  const mapped = ALIYUN_VOICE_MAP[unifiedVoiceId];
+  const mapped = ALIYUN_VOICE_MAP[baseId];
   if (mapped) return mapped;
   // 不在映射表中(如遗留的 'cherry' 等旧 MiniMax 音色)→ 用 CosyVoice 默认音色
   console.warn(`⚠️ [VoiceMap] 未识别的音色ID: "${unifiedVoiceId}",降级使用默认音色 longanhuan_v3`);
@@ -76,7 +260,11 @@ function mapToProviderVoice(unifiedVoiceId: string, providerVendor: string): str
 
 // ============ CosyVoice Instruct 情感控制 ============
 // CosyVoice v3-flash 系统音色支持 7 种情感指令:neutral/happy/sad/angry/fearful/surprised/disgusted
-// 格式:设置情感:{emotion}。(固定格式,不可自定义)
+// 官方格式:
+//   仅情感:你说话的情感是<情感值>。
+//   场景+情感:你正在进行<场景>,你说话的情感是<情感值>。
+//   角色+情感:你现在说话的角色是<角色>,你说话的情感是<情感值>。
+//   身份+情感:你正在以一个<身份>的身份说话,你说话的情感是<情感值>。
 const INSTRUCT_ENABLED = true;
 
 interface EmotionScene {
@@ -154,7 +342,7 @@ const CONTENT_PROFILES: ContentProfile[] = [
 ];
 
 /** 根据文本内容分析情感(兜底:纯情感关键词匹配) */
-function detectEmotion(text: string): string {
+export function detectEmotion(text: string): string {
   const scores: Record<string, number> = {};
   for (const { emotion, keywords } of EMOTION_KEYWORDS) {
     scores[emotion] = 0;
@@ -172,10 +360,10 @@ function detectEmotion(text: string): string {
 
 /**
  * 根据文本内容自动检测最佳音色 + 情感
- * 返回 { voiceId, instructText },voiceId 为统一音色 ID (voice_01~voice_10)
+ * 返回 { voiceId, cosyEmotion, instructText },voiceId 为统一音色 ID (voice_01~voice_10)
  * 当没有任何特征匹配时返回 null(由调用方使用默认音色)
  */
-export function detectBestVoiceAndEmotion(text: string): { voiceId: string; instructText: string } | null {
+export function detectBestVoiceAndEmotion(text: string): { voiceId: string; cosyEmotion: string; instructText: string } | null {
   if (!text || text.length < 20) return null;
 
   const scores = new Map<string, { voiceId: string; emotion: string; score: number }>();
@@ -211,17 +399,35 @@ export function detectBestVoiceAndEmotion(text: string): { voiceId: string; inst
   if (!best || best.score < 2) return null; // 置信度太低,不使用自动选择
 
   console.log(`🔍 [AutoDetect] 内容分析: 最佳音色=${best.voiceId}, 情感=${best.emotion}, 匹配度=${best.score}`);
+  // 基于音色维度 + 检测的情感生成官方 Instruct 指令
+  const baseVoice = UNIFIED_VOICES.find(v => v.id === best!.voiceId);
+  const cosyEmotion = best!.emotion !== 'neutral' ? best!.emotion : undefined;
+  const enrichedVoice: Voice = {
+    ...(baseVoice || { id: best!.voiceId, gender: 'female' as const, name: '', description: '' }),
+    cosyEmotion,
+  };
+  const instructText = generateInstruct(enrichedVoice);
   return {
     voiceId: best.voiceId,
-    instructText: `设置情感:${best.emotion}。`,
+    cosyEmotion: cosyEmotion || 'neutral',
+    instructText: instructText || emotionOnlyInstruct(best.emotion),
   };
 }
 
-/** 根据文本内容动态生成 CosyVoice Instruct 文本(固定格式) */
-export function getVoiceInstruct(text: string): string {
+/** 根据文本内容 + 音色维度动态生成 CosyVoice Instruct 文本(多维度兜底) */
+export function getVoiceInstruct(text: string, voiceId?: string): string {
   const emotion = detectEmotion(text);
-  // CosyVoice v3-flash 系统音色只支持固定格式:设置情感:{emotion}。
-  return `设置情感:${emotion}。`;
+  // 如果有音色维度数据,基于场景+情感生成官方 Instruct 指令
+  if (voiceId) {
+    const baseVoice = UNIFIED_VOICES.find(v => v.id === voiceId) || UNIFIED_VOICES.find(v => v.id === getBaseVoiceId(voiceId));
+    if (baseVoice) {
+      const enriched: Voice = { ...baseVoice, cosyEmotion: emotion !== 'neutral' ? emotion : baseVoice.cosyEmotion };
+      const instruct = generateInstruct(enriched);
+      if (instruct) return instruct;
+    }
+  }
+  // 纯兜底:仅情感指令
+  return emotionOnlyInstruct(emotion);
 }
 
 /** 儿童内容语速(3-6岁儿童友好语速,0.78 为业内经验值) */
@@ -707,13 +913,23 @@ async function processAudioGeneration(
     console.log(`🔊 使用 TTS Provider: ${tts.name} (vendor=${tts.vendor}, mode=${tts.mode})`);
     ttsLogger.debug(`Provider: ${tts.name}, vendor=${tts.vendor}, mode=${tts.mode}`);
 
-    // 🔍 情感检测:优先级 = 用户手动指定 > 内容自动检测 > 兜底情感关键词
-    // 用户手动指定了 emotion → 直接用,跳过所有检测
+    // 🔍 情感检测:优先级 = 用户手动指定 > 音色预设 > 内容自动检测 > 兜底情感关键词
+    // 用户手动指定了 emotion → 直接用,跳过所有检测
     if ((voiceParams as any).emotion) {
       const userEmotion = (voiceParams as any).emotion;
       console.log(`🎭 [Emotion] 用户指定情感: ${userEmotion}`);
       if (INSTRUCT_ENABLED && tts.vendor === 'bailian') {
-        voiceParams = { ...voiceParams, instructText: `设置情感:${userEmotion}。` };
+        voiceParams = { ...voiceParams, instructText: emotionOnlyInstruct(userEmotion) };
+      }
+    }
+
+    // ② 音色自带的预设 instructText(如 voice_01_happy → 你正在进行闲聊互动,你说话的情感是happy。)
+    //    仅在用户未手动指定情感时生效,优先级高于文本自动检测
+    if (INSTRUCT_ENABLED && tts.vendor === 'bailian' && !(voiceParams as any).instructText) {
+      const presetInstruct = getVoiceInstructById(voiceId);
+      if (presetInstruct) {
+        console.log(`🎭 [Instruct] 音色预设指令: ${voiceId} → ${presetInstruct}`);
+        voiceParams = { ...voiceParams, instructText: presetInstruct };
       }
     }
 
@@ -721,7 +937,7 @@ async function processAudioGeneration(
     // 触发条件:voiceId 为空 / 旧系统音色(如'cherry') / 不在统一音色表中
     const isDefaultVoice = !voiceId
       || voiceId === 'cherry'   // 旧系统默认音色
-      || !UNIFIED_VOICES.some(v => v.id === voiceId); // 不在10个统一音色中
+      || !UNIFIED_VOICES.some(v => v.id === voiceId); // 不在22个统一音色中
     let effectiveVoiceId = voiceId;
 
     if (isDefaultVoice) {
@@ -741,7 +957,7 @@ async function processAudioGeneration(
 
     // 为 Aliyun 注入 Instruct 情感控制(兜底:自动检测未触发时用情感关键词兜底)
     if (INSTRUCT_ENABLED && tts.vendor === 'bailian' && !(voiceParams as any).instructText) {
-      const instructText = getVoiceInstruct(text);
+      const instructText = getVoiceInstruct(text, effectiveVoiceId || voiceId);
       if (instructText) {
         voiceParams = { ...voiceParams, instructText };
       }
@@ -1025,7 +1241,7 @@ export async function getAudioStatus(audioId: string): Promise<{ status: string;
   return { status: 'not_found' };
 }
 
-// 获取可用音色(统一10个音色
+// 获取可用音色(22个:10基础 + 12情感变体
 export function getVoices(): Voice[] {
   return UNIFIED_VOICES;
 }
@@ -1389,13 +1605,22 @@ export async function generatePreview(
     }
 
     try {
+      // 注入多维度 Instruct 指令(CosyVoice / Qwen Instruct)
+      const instructToInject = getVoiceInstructById(voiceId);
+      const paramsToUse = instructToInject
+        ? { ...params, instructText: instructToInject }
+        : params;
+      if (instructToInject) {
+        console.log(`🎭 [Preview Instruct] ${voiceId} → ${instructToInject}`);
+      }
+
       // 先创建空文件让 aliyun 下载能定位目录
       const ext = tts.vendor.startsWith('minimax') ? 'mp3' : 'wav';
       const outputPath = path.join(audioDir, `preview.${ext}`);
       if (!fs.existsSync(outputPath)) {
         fs.writeFileSync(outputPath, Buffer.alloc(0));
       }
-      const actualPath = await node.breaker.call(() => tts.synthesize(previewText, voiceName, params, outputPath));
+      const actualPath = await node.breaker.call(() => tts.synthesize(previewText, voiceName, paramsToUse, outputPath));
       const audioUrl = await storageService.uploadAudio(actualPath, audioId);
       console.log(`✅ [TTS Preview] ${tts.name} 生成成功`);
       return { audioId, audioUrl };

+ 23 - 0
server/src/types/index.ts

@@ -111,6 +111,29 @@ export interface Voice {
   gender: 'male' | 'female';
   description: string;
   preview?: string;
+  // ========== 7维度音色描述(维度越丰富,CosyVoice 生成效果越精准)==========
+  /** 维度1:年龄 - 儿童/青少年/青年/中年/老年 */
+  age?: string;
+  /** 维度2:音调 - 高音/偏高/中音/偏低/低音 */
+  pitch?: string;
+  /** 维度3:语速 - 快速/偏快/中速/偏慢/缓慢 */
+  speed?: string;
+  /** 维度4:情感 - 开朗/沉稳/温柔/严肃/活泼/冷静/治愈 */
+  style?: string;
+  /** 维度5:声音特点 - 有磁性/清脆/沙哑/圆润/甜美/浑厚/有力 */
+  characteristics?: string;
+  /** 维度6:适用场景 - 新闻播报/广告配音/有声书/动画角色/语音助手/纪录片解说 */
+  usage?: string;
+  /** CosyVoice 官方 Instruct 场景(闲聊互动/新闻播报/广告促销/比赛解说/一些儿童内容解说/语音导航/脱口秀表演) */
+  scene?: string;
+  /** CosyVoice 官方 Instruct 角色(如:一个旁白) */
+  role?: string;
+  /** CosyVoice 官方 Instruct 身份(如:故事机) */
+  identity?: string;
+  /** 维度7:CosyVoice 底层情感指令(happy/sad/angry/fearful/surprised/disgusted/neutral) */
+  cosyEmotion?: string;
+  /** 预设的 CosyVoice Instruct 指令(为空时自动从维度生成) */
+  instructText?: string;
 }
 
 // 会员权益

+ 105 - 0
speed-picker.yml

@@ -0,0 +1,105 @@
+- generic [ref=e7]:
+  - generic [ref=e8]:
+    - generic [ref=e10]: ⋁
+    - generic [ref=e12]: 小熊过小溪
+    - generic [ref=e13]:
+      - generic [ref=e15]: ⏰
+      - generic [ref=e17]: 📖
+  - generic [ref=e18]:
+    - generic [ref=e20]: 🎵
+    - generic [ref=e21]: 小熊过小溪
+  - generic [ref=e26]:
+    - generic [ref=e29]: "# 小熊过小溪"
+    - generic [ref=e31]: 小熊走啊走,走过绿绿的草地,穿过高高的小树林。
+    - generic [ref=e33]: 忽然,他听到一阵哗哗哗的声音。
+    - generic [ref=e35]: “咦?
+    - generic [ref=e37]: 这是什么声音呀?
+    - generic [ref=e39]: ”
+    - generic [ref=e41]: 小熊走近一看,呀,原来是一条清清的小溪!
+    - generic [ref=e43]: 溪水哗哗地流着,水流轻轻柔柔的,像在唱一首欢快的歌。
+    - generic [ref=e45]: 小熊想走过去,可是低头看看自己的小脚,又看看溪水,心里有点害怕:“水会不会把脚弄湿呀?
+    - generic [ref=e47]: 会不会滑倒呀?
+    - generic [ref=e49]: ”
+    - generic [ref=e51]: 小熊站在溪边想了想,又看了看。
+    - generic [ref=e53]: 咦?
+    - generic [ref=e55]: 溪水里有好多大石头,一块、两块、三块……排列得整整齐齐,像一座小小的石桥。
+    - generic [ref=e57]: “有了!
+    - generic [ref=e59]: 我可以踩着石头过溪呀!
+    - generic [ref=e61]: ”
+    - generic [ref=e63]: 小熊深吸一口气,鼓起勇气,轻轻跳到第一块石头上。
+    - generic [ref=e65]: “一跳!
+    - generic [ref=e67]: ”他稳稳地站住了。
+    - generic [ref=e69]: 溪水在小熊脚边流过,凉凉的,痒痒的。
+    - generic [ref=e71]: 小熊开心极了,又跳到第二块石头上。
+    - generic [ref=e73]: “二跳!
+    - generic [ref=e75]: ”再跳到第三块石头上。
+    - generic [ref=e77]: “三跳!
+    - generic [ref=e79]: ”
+    - generic [ref=e81]: “哈——我成功啦!
+    - generic [ref=e83]: ”
+    - generic [ref=e85]: 小熊站在对岸,开心地摇摇头,甩甩尾巴。
+    - generic [ref=e87]: 他回头看了看小溪,心里美滋滋的:“原来勇敢试一试,就能成功呀!
+    - generic [ref=e89]: ”
+    - generic [ref=e91]: 小熊整理好背包,抬起头,望望远方的山。
+    - generic [ref=e93]: 他相信,翻过那座山,就能找到甜甜的蜂蜜啦!
+    - generic [ref=e95]: 于是,小熊带着快乐的心情,继续往前走。
+  - generic [ref=e96]:
+    - generic [ref=e97]: 0:16
+    - generic [ref=e104]: 1:32
+  - generic [ref=e105]:
+    - generic [ref=e106]:
+      - generic [ref=e108]: 1x
+      - generic [ref=e111]: ⬇
+    - generic [ref=e112]:
+      - generic [ref=e114]: ⏮
+      - generic [ref=e116]: ⏸
+      - generic [ref=e118]: ⏭
+    - generic [ref=e119]:
+      - generic [ref=e121]: 🤍
+      - generic [ref=e122]:
+        - generic [ref=e123]: 📤
+        - generic [ref=e124]: 分享得时长
+  - generic [ref=e125]:
+    - generic [ref=e126]:
+      - generic [ref=e127]:
+        - generic [ref=e128]: 播放列表
+        - generic [ref=e129]: 5首
+      - generic [ref=e130]:
+        - generic [ref=e131]: 🔂
+        - generic [ref=e132]: 顺序
+    - generic [ref=e136]:
+      - generic [ref=e137]:
+        - generic [ref=e138]: "1"
+        - generic [ref=e139]:
+          - generic [ref=e140]: 小熊要出门
+          - generic [ref=e141]: 0:01
+      - generic [ref=e142]:
+        - generic [ref=e143]: "2"
+        - generic [ref=e144]:
+          - generic [ref=e145]: 小熊过小溪
+          - generic [ref=e146]: 1:32
+        - generic [ref=e147]: ▶
+      - generic [ref=e148]:
+        - generic [ref=e149]: "3"
+        - generic [ref=e150]:
+          - generic [ref=e151]: 小熊遇蜜蜂
+          - generic [ref=e152]: 1:00
+      - generic [ref=e153]:
+        - generic [ref=e154]: "4"
+        - generic [ref=e155]:
+          - generic [ref=e156]: 小熊想新办法
+          - generic [ref=e157]: 0:45
+      - generic [ref=e158]:
+        - generic [ref=e159]: "5"
+        - generic [ref=e160]:
+          - generic [ref=e161]: 小熊回家啦
+          - generic [ref=e162]: 1:53
+  - generic [ref=e165]:
+    - generic [ref=e166]: 播放速度
+    - generic [ref=e167]:
+      - generic [ref=e169]: 0.5x
+      - generic [ref=e171]: 0.75x
+      - generic [ref=e173]: 1x
+      - generic [ref=e175]: 1.25x
+      - generic [ref=e177]: 1.5x
+      - generic [ref=e179]: 2x

+ 0 - 4
test-results/.last-run.json

@@ -1,4 +0,0 @@
-{
-  "status": "passed",
-  "failedTests": []
-}

BIN
test-results/.playwright-artifacts-31/page@380cb40bbe1776425196e293c25c7c35.webm


+ 0 - 0
test-results/.playwright-artifacts-31/page@c25e552b876663d7f08a40943363d24d.webm


BIN
test-results/.playwright-artifacts-31/page@f14a75fc673ee94d9f77fde9aeba608a.webm


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-69325878d4bb71a155fc.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-69325878d4bb71a155fc.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-c2236059e45567d9a205-recording1.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-c2236059e45567d9a205-recording1.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-ecc3f1339592f61f7e2c-recording2.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-31/traces/6c3eced42a9de441f456-ecc3f1339592f61f7e2c-recording2.trace


+ 19 - 0
test-results/.playwright-artifacts-31/traces/resources/69560aaf89c66c2a4ced9fd65e13b65da88e389c.html

@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="zh-CN" data-theme="standard">
+  <head>
+    <script type="module" src="/tonggan/@vite/client"></script>
+
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="theme-color" content="#4ecdc4" />
+    <meta name="description" content="tonggan - 儿童注意力训练 App - AI 当教练,爸妈少操心" />
+    <link rel="icon" type="image/svg+xml" href="/tonggan/icon.svg" />
+    <link rel="apple-touch-icon" href="/tonggan/icon.svg" />
+    <link rel="manifest" href="/tonggan/manifest.webmanifest" />
+    <title>tonggan · AI 当教练,爸妈少操心</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/tonggan/src/main.ts?t=1781447310132"></script>
+  </body>
+</html>

BIN
test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447427495.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447427814.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447427859.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447431511.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@380cb40bbe1776425196e293c25c7c35-1781447431552.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@c25e552b876663d7f08a40943363d24d-1781447431996.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@c25e552b876663d7f08a40943363d24d-1781447432305.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@c25e552b876663d7f08a40943363d24d-1781447432355.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447412104.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447412438.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447412507.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447416209.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447416243.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447418965.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447419012.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447421772.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447421785.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447424425.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447424436.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447426994.jpeg


BIN
test-results/.playwright-artifacts-31/traces/resources/page@f14a75fc673ee94d9f77fde9aeba608a-1781447427044.jpeg


BIN
test-results/.playwright-artifacts-36/page@0d8fc5bfd24584490f1a40fc160f0f55.webm


BIN
test-results/.playwright-artifacts-36/page@1c6adc9eb988aa2a4b7fb0b0660b1429.webm


+ 0 - 0
test-results/.playwright-artifacts-36/page@1fa19cee2b4eee15cda1c364fbceb98d.webm


BIN
test-results/.playwright-artifacts-36/page@366cd3f64036e640f7c5caf40de0c114.webm


BIN
test-results/.playwright-artifacts-36/page@38d6de42543c8796452cf60e0c8928e5.webm


BIN
test-results/.playwright-artifacts-36/page@449bf494d9e306424c69e104b3f13a80.webm


BIN
test-results/.playwright-artifacts-36/page@6bc61f2730b3a0b001e38b7de74759f5.webm


BIN
test-results/.playwright-artifacts-36/page@88275522e516e813f4307dbf1faec8ce.webm


BIN
test-results/.playwright-artifacts-36/page@d5a2d39b7ee0ad1293c230a506ea8b69.webm


BIN
test-results/.playwright-artifacts-36/page@e75cac63153e30886cd7e20c0b57e7d7.webm


BIN
test-results/.playwright-artifacts-36/page@ed17e53a7643634decc5f2abb45ccfbe.webm


BIN
test-results/.playwright-artifacts-36/page@ef1b7eba78663d2265d2891ecbfdff74.webm


BIN
test-results/.playwright-artifacts-36/page@efdd6f61ebac54ae6afcfaa86db3158a.webm


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-0b89fa8e0daae3b7238d-recording4.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-0b89fa8e0daae3b7238d-recording4.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-12431a3b247fe64b1787-recording3.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-12431a3b247fe64b1787-recording3.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-43b5e9ef614a381f0334.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-43b5e9ef614a381f0334.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-6b63c33ef0850b90273d-recording5.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 13 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-6b63c33ef0850b90273d-recording5.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-7526d11532d6e5e4e1e9-recording11.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-7526d11532d6e5e4e1e9-recording11.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b080e21dac605b44266a-recording1.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b080e21dac605b44266a-recording1.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b1861679fe32ba5633f2-recording6.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-b1861679fe32ba5633f2-recording6.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-d3e7bede407937130a8d-recording9.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-d3e7bede407937130a8d-recording9.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e675d85849828b7ee09b-recording8.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e675d85849828b7ee09b-recording8.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e8d262750fea59dfa098-recording12.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-e8d262750fea59dfa098-recording12.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f2f287e9ec0b5eef3dc4-recording7.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f2f287e9ec0b5eef3dc4-recording7.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f85d86480af68b39727b-recording2.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-f85d86480af68b39727b-recording2.trace


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-ff309a1eab8f4d3ff88d-recording10.network


Diferenças do arquivo suprimidas por serem muito extensas
+ 14 - 0
test-results/.playwright-artifacts-36/traces/6c3eced42a9de441f456-ff309a1eab8f4d3ff88d-recording10.trace


+ 19 - 0
test-results/.playwright-artifacts-36/traces/resources/69560aaf89c66c2a4ced9fd65e13b65da88e389c.html

@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="zh-CN" data-theme="standard">
+  <head>
+    <script type="module" src="/tonggan/@vite/client"></script>
+
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="theme-color" content="#4ecdc4" />
+    <meta name="description" content="tonggan - 儿童注意力训练 App - AI 当教练,爸妈少操心" />
+    <link rel="icon" type="image/svg+xml" href="/tonggan/icon.svg" />
+    <link rel="apple-touch-icon" href="/tonggan/icon.svg" />
+    <link rel="manifest" href="/tonggan/manifest.webmanifest" />
+    <title>tonggan · AI 当教练,爸妈少操心</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/tonggan/src/main.ts?t=1781447310132"></script>
+  </body>
+</html>

BIN
test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447403998.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447404304.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447404364.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447407932.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@0d8fc5bfd24584490f1a40fc160f0f55-1781447407968.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447437800.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447437825.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447437870.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447441675.jpeg


BIN
test-results/.playwright-artifacts-36/traces/resources/page@1c6adc9eb988aa2a4b7fb0b0660b1429-1781447441741.jpeg


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff