liumaolin commited on
Commit
14b5955
·
1 Parent(s): 9f9c2f8

feat(tts-voice-app): add model download setup page for first-time initialization

Browse files

- Add initial setup page (ModelSetup.vue) with download progress UI
- Implement model download manager in main process with progress tracking
- Add setup store for managing download state and progress
- Integrate model check on app startup, redirect to setup if needed
- Add IPC handlers for model download, extraction, and status checks
- Support downloading 4 models from ModelScope (9.09 GB total):
* GPT-SoVITS pretrained models (4.56 GB)
* G2PW Chinese pronunciation model (588.86 MB)
* FunASR speech recognition model (1.09 GB)
* Faster Whisper multilingual ASR model (2.85 GB)
- Add i18n support for setup page (zh-CN, en-US)
- Install extract-zip and tar dependencies for archive extraction
- Models saved to ~/.moyoyo-tts/models directory

tts-voice-app/package-lock.json CHANGED
@@ -11,7 +11,9 @@
11
  "dependencies": {
12
  "axios": "^1.6.0",
13
  "element-plus": "^2.5.0",
 
14
  "pinia": "^2.1.7",
 
15
  "vue": "^3.4.0",
16
  "vue-i18n": "^9.9.0",
17
  "vue-router": "^4.2.5"
@@ -1185,6 +1187,27 @@
1185
  "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1186
  }
1187
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
  "node_modules/@jridgewell/gen-mapping": {
1189
  "version": "0.3.13",
1190
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -2122,7 +2145,7 @@
2122
  "version": "20.19.30",
2123
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
2124
  "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
2125
- "dev": true,
2126
  "license": "MIT",
2127
  "dependencies": {
2128
  "undici-types": "~6.21.0"
@@ -2168,7 +2191,6 @@
2168
  "version": "2.10.3",
2169
  "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
2170
  "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
2171
- "dev": true,
2172
  "license": "MIT",
2173
  "optional": true,
2174
  "dependencies": {
@@ -2538,6 +2560,16 @@
2538
  "electron-builder-squirrel-windows": "24.13.3"
2539
  }
2540
  },
 
 
 
 
 
 
 
 
 
 
2541
  "node_modules/app-builder-lib/node_modules/fs-extra": {
2542
  "version": "10.1.0",
2543
  "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -2566,6 +2598,33 @@
2566
  "graceful-fs": "^4.1.6"
2567
  }
2568
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2569
  "node_modules/app-builder-lib/node_modules/semver": {
2570
  "version": "7.7.3",
2571
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -2579,6 +2638,25 @@
2579
  "node": ">=10"
2580
  }
2581
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2582
  "node_modules/app-builder-lib/node_modules/universalify": {
2583
  "version": "2.0.1",
2584
  "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -2589,6 +2667,13 @@
2589
  "node": ">= 10.0.0"
2590
  }
2591
  },
 
 
 
 
 
 
 
2592
  "node_modules/archiver": {
2593
  "version": "5.3.2",
2594
  "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
@@ -2892,7 +2977,6 @@
2892
  "version": "0.2.13",
2893
  "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
2894
  "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
2895
- "dev": true,
2896
  "license": "MIT",
2897
  "engines": {
2898
  "node": "*"
@@ -3102,13 +3186,12 @@
3102
  }
3103
  },
3104
  "node_modules/chownr": {
3105
- "version": "2.0.0",
3106
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
3107
- "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
3108
- "dev": true,
3109
- "license": "ISC",
3110
  "engines": {
3111
- "node": ">=10"
3112
  }
3113
  },
3114
  "node_modules/chromium-pickle-js": {
@@ -3410,7 +3493,6 @@
3410
  "version": "4.4.3",
3411
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
3412
  "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
3413
- "dev": true,
3414
  "license": "MIT",
3415
  "dependencies": {
3416
  "ms": "^2.1.3"
@@ -3570,6 +3652,7 @@
3570
  "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
3571
  "dev": true,
3572
  "license": "MIT",
 
3573
  "dependencies": {
3574
  "app-builder-lib": "24.13.3",
3575
  "builder-util": "24.13.1",
@@ -3981,7 +4064,6 @@
3981
  "version": "1.4.5",
3982
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
3983
  "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
3984
- "dev": true,
3985
  "license": "MIT",
3986
  "dependencies": {
3987
  "once": "^1.4.0"
@@ -4142,7 +4224,6 @@
4142
  "version": "2.0.1",
4143
  "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
4144
  "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
4145
- "dev": true,
4146
  "license": "BSD-2-Clause",
4147
  "dependencies": {
4148
  "debug": "^4.1.1",
@@ -4188,7 +4269,6 @@
4188
  "version": "1.1.0",
4189
  "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
4190
  "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
4191
- "dev": true,
4192
  "license": "MIT",
4193
  "dependencies": {
4194
  "pend": "~1.2.0"
@@ -4404,7 +4484,6 @@
4404
  "version": "5.2.0",
4405
  "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
4406
  "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
4407
- "dev": true,
4408
  "license": "MIT",
4409
  "dependencies": {
4410
  "pump": "^3.0.0"
@@ -5238,39 +5317,26 @@
5238
  }
5239
  },
5240
  "node_modules/minizlib": {
5241
- "version": "2.1.2",
5242
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
5243
- "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
5244
- "dev": true,
5245
  "license": "MIT",
5246
  "dependencies": {
5247
- "minipass": "^3.0.0",
5248
- "yallist": "^4.0.0"
5249
  },
5250
  "engines": {
5251
- "node": ">= 8"
5252
  }
5253
  },
5254
  "node_modules/minizlib/node_modules/minipass": {
5255
- "version": "3.3.6",
5256
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
5257
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
5258
- "dev": true,
5259
  "license": "ISC",
5260
- "dependencies": {
5261
- "yallist": "^4.0.0"
5262
- },
5263
  "engines": {
5264
- "node": ">=8"
5265
  }
5266
  },
5267
- "node_modules/minizlib/node_modules/yallist": {
5268
- "version": "4.0.0",
5269
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
5270
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
5271
- "dev": true,
5272
- "license": "ISC"
5273
- },
5274
  "node_modules/mkdirp": {
5275
  "version": "1.0.4",
5276
  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -5288,7 +5354,6 @@
5288
  "version": "2.1.3",
5289
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
5290
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
5291
- "dev": true,
5292
  "license": "MIT"
5293
  },
5294
  "node_modules/muggle-string": {
@@ -5375,7 +5440,6 @@
5375
  "version": "1.4.0",
5376
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
5377
  "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
5378
- "dev": true,
5379
  "license": "ISC",
5380
  "dependencies": {
5381
  "wrappy": "1"
@@ -5453,7 +5517,6 @@
5453
  "version": "1.2.0",
5454
  "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
5455
  "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
5456
- "dev": true,
5457
  "license": "MIT"
5458
  },
5459
  "node_modules/picocolors": {
@@ -5582,7 +5645,6 @@
5582
  "version": "3.0.3",
5583
  "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
5584
  "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
5585
- "dev": true,
5586
  "license": "MIT",
5587
  "dependencies": {
5588
  "end-of-stream": "^1.1.0",
@@ -6111,21 +6173,19 @@
6111
  }
6112
  },
6113
  "node_modules/tar": {
6114
- "version": "6.2.1",
6115
- "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
6116
- "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
6117
- "dev": true,
6118
- "license": "ISC",
6119
  "dependencies": {
6120
- "chownr": "^2.0.0",
6121
- "fs-minipass": "^2.0.0",
6122
- "minipass": "^5.0.0",
6123
- "minizlib": "^2.1.1",
6124
- "mkdirp": "^1.0.3",
6125
- "yallist": "^4.0.0"
6126
  },
6127
  "engines": {
6128
- "node": ">=10"
6129
  }
6130
  },
6131
  "node_modules/tar-stream": {
@@ -6145,12 +6205,23 @@
6145
  "node": ">=6"
6146
  }
6147
  },
 
 
 
 
 
 
 
 
 
6148
  "node_modules/tar/node_modules/yallist": {
6149
- "version": "4.0.0",
6150
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
6151
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
6152
- "dev": true,
6153
- "license": "ISC"
 
 
6154
  },
6155
  "node_modules/temp-file": {
6156
  "version": "3.4.0",
@@ -6264,7 +6335,7 @@
6264
  "version": "6.21.0",
6265
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
6266
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
6267
- "dev": true,
6268
  "license": "MIT"
6269
  },
6270
  "node_modules/universalify": {
@@ -6592,7 +6663,6 @@
6592
  "version": "1.0.2",
6593
  "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
6594
  "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
6595
- "dev": true,
6596
  "license": "ISC"
6597
  },
6598
  "node_modules/xmlbuilder": {
@@ -6655,7 +6725,6 @@
6655
  "version": "2.10.0",
6656
  "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
6657
  "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
6658
- "dev": true,
6659
  "license": "MIT",
6660
  "dependencies": {
6661
  "buffer-crc32": "~0.2.3",
 
11
  "dependencies": {
12
  "axios": "^1.6.0",
13
  "element-plus": "^2.5.0",
14
+ "extract-zip": "^2.0.1",
15
  "pinia": "^2.1.7",
16
+ "tar": "^7.5.6",
17
  "vue": "^3.4.0",
18
  "vue-i18n": "^9.9.0",
19
  "vue-router": "^4.2.5"
 
1187
  "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1188
  }
1189
  },
1190
+ "node_modules/@isaacs/fs-minipass": {
1191
+ "version": "4.0.1",
1192
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
1193
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
1194
+ "license": "ISC",
1195
+ "dependencies": {
1196
+ "minipass": "^7.0.4"
1197
+ },
1198
+ "engines": {
1199
+ "node": ">=18.0.0"
1200
+ }
1201
+ },
1202
+ "node_modules/@isaacs/fs-minipass/node_modules/minipass": {
1203
+ "version": "7.1.2",
1204
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
1205
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
1206
+ "license": "ISC",
1207
+ "engines": {
1208
+ "node": ">=16 || 14 >=14.17"
1209
+ }
1210
+ },
1211
  "node_modules/@jridgewell/gen-mapping": {
1212
  "version": "0.3.13",
1213
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
 
2145
  "version": "20.19.30",
2146
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
2147
  "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
2148
+ "devOptional": true,
2149
  "license": "MIT",
2150
  "dependencies": {
2151
  "undici-types": "~6.21.0"
 
2191
  "version": "2.10.3",
2192
  "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
2193
  "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
 
2194
  "license": "MIT",
2195
  "optional": true,
2196
  "dependencies": {
 
2560
  "electron-builder-squirrel-windows": "24.13.3"
2561
  }
2562
  },
2563
+ "node_modules/app-builder-lib/node_modules/chownr": {
2564
+ "version": "2.0.0",
2565
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
2566
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
2567
+ "dev": true,
2568
+ "license": "ISC",
2569
+ "engines": {
2570
+ "node": ">=10"
2571
+ }
2572
+ },
2573
  "node_modules/app-builder-lib/node_modules/fs-extra": {
2574
  "version": "10.1.0",
2575
  "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
 
2598
  "graceful-fs": "^4.1.6"
2599
  }
2600
  },
2601
+ "node_modules/app-builder-lib/node_modules/minizlib": {
2602
+ "version": "2.1.2",
2603
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
2604
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
2605
+ "dev": true,
2606
+ "license": "MIT",
2607
+ "dependencies": {
2608
+ "minipass": "^3.0.0",
2609
+ "yallist": "^4.0.0"
2610
+ },
2611
+ "engines": {
2612
+ "node": ">= 8"
2613
+ }
2614
+ },
2615
+ "node_modules/app-builder-lib/node_modules/minizlib/node_modules/minipass": {
2616
+ "version": "3.3.6",
2617
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
2618
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
2619
+ "dev": true,
2620
+ "license": "ISC",
2621
+ "dependencies": {
2622
+ "yallist": "^4.0.0"
2623
+ },
2624
+ "engines": {
2625
+ "node": ">=8"
2626
+ }
2627
+ },
2628
  "node_modules/app-builder-lib/node_modules/semver": {
2629
  "version": "7.7.3",
2630
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
 
2638
  "node": ">=10"
2639
  }
2640
  },
2641
+ "node_modules/app-builder-lib/node_modules/tar": {
2642
+ "version": "6.2.1",
2643
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
2644
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
2645
+ "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me",
2646
+ "dev": true,
2647
+ "license": "ISC",
2648
+ "dependencies": {
2649
+ "chownr": "^2.0.0",
2650
+ "fs-minipass": "^2.0.0",
2651
+ "minipass": "^5.0.0",
2652
+ "minizlib": "^2.1.1",
2653
+ "mkdirp": "^1.0.3",
2654
+ "yallist": "^4.0.0"
2655
+ },
2656
+ "engines": {
2657
+ "node": ">=10"
2658
+ }
2659
+ },
2660
  "node_modules/app-builder-lib/node_modules/universalify": {
2661
  "version": "2.0.1",
2662
  "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
 
2667
  "node": ">= 10.0.0"
2668
  }
2669
  },
2670
+ "node_modules/app-builder-lib/node_modules/yallist": {
2671
+ "version": "4.0.0",
2672
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
2673
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
2674
+ "dev": true,
2675
+ "license": "ISC"
2676
+ },
2677
  "node_modules/archiver": {
2678
  "version": "5.3.2",
2679
  "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
 
2977
  "version": "0.2.13",
2978
  "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
2979
  "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
 
2980
  "license": "MIT",
2981
  "engines": {
2982
  "node": "*"
 
3186
  }
3187
  },
3188
  "node_modules/chownr": {
3189
+ "version": "3.0.0",
3190
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
3191
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
3192
+ "license": "BlueOak-1.0.0",
 
3193
  "engines": {
3194
+ "node": ">=18"
3195
  }
3196
  },
3197
  "node_modules/chromium-pickle-js": {
 
3493
  "version": "4.4.3",
3494
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
3495
  "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
 
3496
  "license": "MIT",
3497
  "dependencies": {
3498
  "ms": "^2.1.3"
 
3652
  "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
3653
  "dev": true,
3654
  "license": "MIT",
3655
+ "peer": true,
3656
  "dependencies": {
3657
  "app-builder-lib": "24.13.3",
3658
  "builder-util": "24.13.1",
 
4064
  "version": "1.4.5",
4065
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
4066
  "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
 
4067
  "license": "MIT",
4068
  "dependencies": {
4069
  "once": "^1.4.0"
 
4224
  "version": "2.0.1",
4225
  "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
4226
  "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
 
4227
  "license": "BSD-2-Clause",
4228
  "dependencies": {
4229
  "debug": "^4.1.1",
 
4269
  "version": "1.1.0",
4270
  "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
4271
  "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
 
4272
  "license": "MIT",
4273
  "dependencies": {
4274
  "pend": "~1.2.0"
 
4484
  "version": "5.2.0",
4485
  "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
4486
  "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
 
4487
  "license": "MIT",
4488
  "dependencies": {
4489
  "pump": "^3.0.0"
 
5317
  }
5318
  },
5319
  "node_modules/minizlib": {
5320
+ "version": "3.1.0",
5321
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
5322
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
 
5323
  "license": "MIT",
5324
  "dependencies": {
5325
+ "minipass": "^7.1.2"
 
5326
  },
5327
  "engines": {
5328
+ "node": ">= 18"
5329
  }
5330
  },
5331
  "node_modules/minizlib/node_modules/minipass": {
5332
+ "version": "7.1.2",
5333
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
5334
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
 
5335
  "license": "ISC",
 
 
 
5336
  "engines": {
5337
+ "node": ">=16 || 14 >=14.17"
5338
  }
5339
  },
 
 
 
 
 
 
 
5340
  "node_modules/mkdirp": {
5341
  "version": "1.0.4",
5342
  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
 
5354
  "version": "2.1.3",
5355
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
5356
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
 
5357
  "license": "MIT"
5358
  },
5359
  "node_modules/muggle-string": {
 
5440
  "version": "1.4.0",
5441
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
5442
  "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
 
5443
  "license": "ISC",
5444
  "dependencies": {
5445
  "wrappy": "1"
 
5517
  "version": "1.2.0",
5518
  "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
5519
  "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
 
5520
  "license": "MIT"
5521
  },
5522
  "node_modules/picocolors": {
 
5645
  "version": "3.0.3",
5646
  "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
5647
  "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
 
5648
  "license": "MIT",
5649
  "dependencies": {
5650
  "end-of-stream": "^1.1.0",
 
6173
  }
6174
  },
6175
  "node_modules/tar": {
6176
+ "version": "7.5.6",
6177
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
6178
+ "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
6179
+ "license": "BlueOak-1.0.0",
 
6180
  "dependencies": {
6181
+ "@isaacs/fs-minipass": "^4.0.0",
6182
+ "chownr": "^3.0.0",
6183
+ "minipass": "^7.1.2",
6184
+ "minizlib": "^3.1.0",
6185
+ "yallist": "^5.0.0"
 
6186
  },
6187
  "engines": {
6188
+ "node": ">=18"
6189
  }
6190
  },
6191
  "node_modules/tar-stream": {
 
6205
  "node": ">=6"
6206
  }
6207
  },
6208
+ "node_modules/tar/node_modules/minipass": {
6209
+ "version": "7.1.2",
6210
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
6211
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
6212
+ "license": "ISC",
6213
+ "engines": {
6214
+ "node": ">=16 || 14 >=14.17"
6215
+ }
6216
+ },
6217
  "node_modules/tar/node_modules/yallist": {
6218
+ "version": "5.0.0",
6219
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
6220
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
6221
+ "license": "BlueOak-1.0.0",
6222
+ "engines": {
6223
+ "node": ">=18"
6224
+ }
6225
  },
6226
  "node_modules/temp-file": {
6227
  "version": "3.4.0",
 
6335
  "version": "6.21.0",
6336
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
6337
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
6338
+ "devOptional": true,
6339
  "license": "MIT"
6340
  },
6341
  "node_modules/universalify": {
 
6663
  "version": "1.0.2",
6664
  "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
6665
  "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
 
6666
  "license": "ISC"
6667
  },
6668
  "node_modules/xmlbuilder": {
 
6725
  "version": "2.10.0",
6726
  "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
6727
  "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
 
6728
  "license": "MIT",
6729
  "dependencies": {
6730
  "buffer-crc32": "~0.2.3",
tts-voice-app/package.json CHANGED
@@ -14,24 +14,26 @@
14
  "build:linux": "npm run build && electron-builder --linux --config"
15
  },
16
  "dependencies": {
17
- "vue": "^3.4.0",
18
- "vue-router": "^4.2.5",
19
- "pinia": "^2.1.7",
20
- "element-plus": "^2.5.0",
21
  "axios": "^1.6.0",
22
- "vue-i18n": "^9.9.0"
 
 
 
 
 
 
23
  },
24
  "devDependencies": {
25
  "@electron-toolkit/preload": "^3.0.0",
26
  "@electron-toolkit/utils": "^3.0.0",
 
27
  "@vitejs/plugin-vue": "^5.0.0",
28
  "electron": "^28.0.0",
29
- "electron-vite": "^2.0.0",
30
  "electron-builder": "^24.9.1",
 
31
  "sass": "^1.69.0",
32
  "typescript": "^5.3.0",
33
  "vite": "^5.0.0",
34
- "vue-tsc": "^1.8.0",
35
- "@types/node": "^20.10.0"
36
  }
37
  }
 
14
  "build:linux": "npm run build && electron-builder --linux --config"
15
  },
16
  "dependencies": {
 
 
 
 
17
  "axios": "^1.6.0",
18
+ "element-plus": "^2.5.0",
19
+ "extract-zip": "^2.0.1",
20
+ "pinia": "^2.1.7",
21
+ "tar": "^7.5.6",
22
+ "vue": "^3.4.0",
23
+ "vue-i18n": "^9.9.0",
24
+ "vue-router": "^4.2.5"
25
  },
26
  "devDependencies": {
27
  "@electron-toolkit/preload": "^3.0.0",
28
  "@electron-toolkit/utils": "^3.0.0",
29
+ "@types/node": "^20.10.0",
30
  "@vitejs/plugin-vue": "^5.0.0",
31
  "electron": "^28.0.0",
 
32
  "electron-builder": "^24.9.1",
33
+ "electron-vite": "^2.0.0",
34
  "sass": "^1.69.0",
35
  "typescript": "^5.3.0",
36
  "vite": "^5.0.0",
37
+ "vue-tsc": "^1.8.0"
 
38
  }
39
  }
tts-voice-app/src/main/index.ts CHANGED
@@ -1,18 +1,280 @@
1
  import { app, shell, BrowserWindow, ipcMain, protocol, dialog } from 'electron'
2
  import { join } from 'path'
3
  import { homedir, tmpdir, platform } from 'os'
4
- import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync, copyFileSync } from 'fs'
5
  import { electronApp, optimizer, is } from '@electron-toolkit/utils'
6
  import { spawn } from 'child_process'
7
  import https from 'https'
8
  import http from 'http'
 
 
9
 
10
  // Voice storage directory
11
  const VOICE_STORAGE_DIR = join(homedir(), '.moyoyo-tts', 'voices')
12
 
 
 
 
13
  // GPT-SoVITS project root directory (parent of tts-voice-app)
14
  const GPT_SOVITS_ROOT = join(__dirname, '..', '..', '..')
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  // Get Python executable path based on platform
17
  function getPythonPath(): string {
18
  const isWindows = platform() === 'win32'
@@ -120,7 +382,7 @@ interface VoiceInfo {
120
  }
121
 
122
  function createWindow(): void {
123
- const mainWindow = new BrowserWindow({
124
  width: 1200,
125
  height: 800,
126
  minWidth: 900,
@@ -137,7 +399,7 @@ function createWindow(): void {
137
  })
138
 
139
  mainWindow.on('ready-to-show', () => {
140
- mainWindow.show()
141
  })
142
 
143
  mainWindow.webContents.setWindowOpenHandler((details) => {
@@ -230,6 +492,62 @@ app.whenReady().then(() => {
230
  return process.env['API_BASE_URL'] || 'http://localhost:8000'
231
  })
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  // IPC: 保存音色文件到本地
234
  ipcMain.handle('save-voice-files', async (_, request: SaveVoiceFilesRequest) => {
235
  try {
 
1
  import { app, shell, BrowserWindow, ipcMain, protocol, dialog } from 'electron'
2
  import { join } from 'path'
3
  import { homedir, tmpdir, platform } from 'os'
4
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync, copyFileSync, createWriteStream } from 'fs'
5
  import { electronApp, optimizer, is } from '@electron-toolkit/utils'
6
  import { spawn } from 'child_process'
7
  import https from 'https'
8
  import http from 'http'
9
+ import extractZip from 'extract-zip'
10
+ import * as tar from 'tar'
11
 
12
  // Voice storage directory
13
  const VOICE_STORAGE_DIR = join(homedir(), '.moyoyo-tts', 'voices')
14
 
15
+ // Models storage directory
16
+ const MODELS_DIR = join(homedir(), '.moyoyo-tts', 'models')
17
+
18
  // GPT-SoVITS project root directory (parent of tts-voice-app)
19
  const GPT_SOVITS_ROOT = join(__dirname, '..', '..', '..')
20
 
21
+ // Model files configuration
22
+ interface ModelFileConfig {
23
+ id: string
24
+ name: string
25
+ description: string
26
+ url: string
27
+ extractTo: string
28
+ checkPath: string // Path to check if model is already extracted
29
+ size?: string // Estimated file size for display
30
+ }
31
+
32
+ const MODEL_FILES: ModelFileConfig[] = [
33
+ {
34
+ id: 'pretrained_models',
35
+ name: 'GPT-SoVITS 预训练模型',
36
+ description: '核心预训练模型文件,包含语音合成所需的基础模型',
37
+ url: 'https://www.modelscope.cn/models/XXXXRT/GPT-SoVITS-Pretrained/resolve/master/pretrained_models.zip',
38
+ extractTo: 'pretrained_models',
39
+ checkPath: 'pretrained_models/sv',
40
+ size: '4.56 GB'
41
+ },
42
+ {
43
+ id: 'g2pw',
44
+ name: 'G2PW 模型',
45
+ description: '中文拼音转换模型,用于文本到语音的发音转换',
46
+ url: 'https://www.modelscope.cn/models/XXXXRT/GPT-SoVITS-Pretrained/resolve/master/G2PWModel.zip',
47
+ extractTo: 'G2PWModel',
48
+ checkPath: 'G2PWModel',
49
+ size: '588.86 MB'
50
+ },
51
+ {
52
+ id: 'funasr',
53
+ name: 'FunASR 模型',
54
+ description: '阿里达摩院语音识别模型,用于音频转录',
55
+ url: 'https://www.modelscope.cn/models/XXXXRT/GPT-SoVITS-Pretrained/resolve/master/funasr.zip',
56
+ extractTo: 'funasr',
57
+ checkPath: 'funasr',
58
+ size: '1.09 GB'
59
+ },
60
+ {
61
+ id: 'faster_whisper',
62
+ name: 'Faster Whisper 模型',
63
+ description: 'OpenAI Whisper 加速版,用于多语言语音识别',
64
+ url: 'https://www.modelscope.cn/models/XXXXRT/GPT-SoVITS-Pretrained/resolve/master/faster-whisper.zip',
65
+ extractTo: 'faster-whisper',
66
+ checkPath: 'faster-whisper',
67
+ size: '2.85 GB'
68
+ }
69
+ ]
70
+
71
+ // Model download status
72
+ interface ModelStatus {
73
+ id: string
74
+ name: string
75
+ description: string
76
+ size?: string
77
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error'
78
+ progress: number
79
+ error?: string
80
+ }
81
+
82
+ // Store main window reference for sending progress updates
83
+ let mainWindow: BrowserWindow | null = null
84
+
85
+ // Ensure models directory exists
86
+ function ensureModelsDir(): void {
87
+ if (!existsSync(MODELS_DIR)) {
88
+ mkdirSync(MODELS_DIR, { recursive: true })
89
+ }
90
+ }
91
+
92
+ // Check if all models are installed
93
+ function checkModelsInstalled(): { allInstalled: boolean; models: ModelStatus[] } {
94
+ ensureModelsDir()
95
+
96
+ const models: ModelStatus[] = MODEL_FILES.map(model => {
97
+ const checkFullPath = join(MODELS_DIR, model.checkPath)
98
+ const isInstalled = existsSync(checkFullPath)
99
+
100
+ return {
101
+ id: model.id,
102
+ name: model.name,
103
+ description: model.description,
104
+ size: model.size,
105
+ status: isInstalled ? 'completed' : 'pending',
106
+ progress: isInstalled ? 100 : 0
107
+ }
108
+ })
109
+
110
+ const allInstalled = models.every(m => m.status === 'completed')
111
+
112
+ return { allInstalled, models }
113
+ }
114
+
115
+ // Download file with progress
116
+ function downloadFileWithProgress(
117
+ url: string,
118
+ destPath: string,
119
+ modelId: string,
120
+ onProgress: (progress: number) => void
121
+ ): Promise<void> {
122
+ return new Promise((resolve, reject) => {
123
+ const file = createWriteStream(destPath)
124
+
125
+ const request = https.get(url, {
126
+ headers: {
127
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
128
+ }
129
+ }, (response) => {
130
+ // Handle redirects
131
+ if (response.statusCode === 301 || response.statusCode === 302) {
132
+ const redirectUrl = response.headers.location
133
+ if (redirectUrl) {
134
+ file.close()
135
+ rmSync(destPath, { force: true })
136
+ downloadFileWithProgress(redirectUrl, destPath, modelId, onProgress)
137
+ .then(resolve)
138
+ .catch(reject)
139
+ return
140
+ }
141
+ }
142
+
143
+ if (response.statusCode !== 200) {
144
+ file.close()
145
+ rmSync(destPath, { force: true })
146
+ reject(new Error(`下载失败: HTTP ${response.statusCode}`))
147
+ return
148
+ }
149
+
150
+ const totalSize = parseInt(response.headers['content-length'] || '0', 10)
151
+ let downloadedSize = 0
152
+
153
+ response.on('data', (chunk) => {
154
+ downloadedSize += chunk.length
155
+ if (totalSize > 0) {
156
+ const progress = Math.round((downloadedSize / totalSize) * 100)
157
+ onProgress(progress)
158
+ }
159
+ })
160
+
161
+ response.pipe(file)
162
+
163
+ file.on('finish', () => {
164
+ file.close()
165
+ resolve()
166
+ })
167
+
168
+ file.on('error', (err) => {
169
+ file.close()
170
+ rmSync(destPath, { force: true })
171
+ reject(err)
172
+ })
173
+ })
174
+
175
+ request.on('error', (err) => {
176
+ file.close()
177
+ rmSync(destPath, { force: true })
178
+ reject(err)
179
+ })
180
+
181
+ request.setTimeout(60000, () => {
182
+ request.destroy()
183
+ file.close()
184
+ rmSync(destPath, { force: true })
185
+ reject(new Error('下载超时'))
186
+ })
187
+ })
188
+ }
189
+
190
+ // Extract downloaded file
191
+ async function extractFile(filePath: string, extractTo: string): Promise<void> {
192
+ const targetDir = join(MODELS_DIR, extractTo)
193
+
194
+ // Create target directory
195
+ if (!existsSync(targetDir)) {
196
+ mkdirSync(targetDir, { recursive: true })
197
+ }
198
+
199
+ if (filePath.endsWith('.zip')) {
200
+ // Extract ZIP file
201
+ await extractZip(filePath, { dir: MODELS_DIR })
202
+ } else if (filePath.endsWith('.tar.gz') || filePath.endsWith('.tgz')) {
203
+ // Extract TAR.GZ file
204
+ await tar.extract({
205
+ file: filePath,
206
+ cwd: targetDir
207
+ })
208
+ } else {
209
+ throw new Error(`不支持的文件格式: ${filePath}`)
210
+ }
211
+
212
+ // Clean up downloaded archive
213
+ rmSync(filePath, { force: true })
214
+ }
215
+
216
+ // Download and extract a single model
217
+ async function downloadModel(modelId: string): Promise<void> {
218
+ const model = MODEL_FILES.find(m => m.id === modelId)
219
+ if (!model) {
220
+ throw new Error(`未知的模型: ${modelId}`)
221
+ }
222
+
223
+ ensureModelsDir()
224
+
225
+ // Determine file extension from URL
226
+ const isZip = model.url.endsWith('.zip')
227
+ const isTarGz = model.url.endsWith('.tar.gz') || model.url.endsWith('.tgz')
228
+ const ext = isZip ? '.zip' : isTarGz ? '.tar.gz' : ''
229
+
230
+ const tempFilePath = join(MODELS_DIR, `${modelId}${ext}`)
231
+
232
+ // Send status update: downloading
233
+ sendModelProgress(modelId, 'downloading', 0)
234
+
235
+ try {
236
+ // Download file
237
+ await downloadFileWithProgress(model.url, tempFilePath, modelId, (progress) => {
238
+ sendModelProgress(modelId, 'downloading', progress)
239
+ })
240
+
241
+ // Send status update: extracting
242
+ sendModelProgress(modelId, 'extracting', 100)
243
+
244
+ // Extract file
245
+ await extractFile(tempFilePath, model.extractTo)
246
+
247
+ // Send status update: completed
248
+ sendModelProgress(modelId, 'completed', 100)
249
+ } catch (error) {
250
+ // Clean up on error
251
+ if (existsSync(tempFilePath)) {
252
+ rmSync(tempFilePath, { force: true })
253
+ }
254
+
255
+ const errorMessage = error instanceof Error ? error.message : '下载失败'
256
+ sendModelProgress(modelId, 'error', 0, errorMessage)
257
+ throw error
258
+ }
259
+ }
260
+
261
+ // Send progress update to renderer
262
+ function sendModelProgress(
263
+ modelId: string,
264
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error',
265
+ progress: number,
266
+ error?: string
267
+ ): void {
268
+ if (mainWindow && !mainWindow.isDestroyed()) {
269
+ mainWindow.webContents.send('model-download-progress', {
270
+ modelId,
271
+ status,
272
+ progress,
273
+ error
274
+ })
275
+ }
276
+ }
277
+
278
  // Get Python executable path based on platform
279
  function getPythonPath(): string {
280
  const isWindows = platform() === 'win32'
 
382
  }
383
 
384
  function createWindow(): void {
385
+ mainWindow = new BrowserWindow({
386
  width: 1200,
387
  height: 800,
388
  minWidth: 900,
 
399
  })
400
 
401
  mainWindow.on('ready-to-show', () => {
402
+ mainWindow?.show()
403
  })
404
 
405
  mainWindow.webContents.setWindowOpenHandler((details) => {
 
492
  return process.env['API_BASE_URL'] || 'http://localhost:8000'
493
  })
494
 
495
+ // IPC: 检查模型安装状态
496
+ ipcMain.handle('check-models-status', () => {
497
+ return checkModelsInstalled()
498
+ })
499
+
500
+ // IPC: 获取模型配置列表
501
+ ipcMain.handle('get-model-configs', () => {
502
+ return MODEL_FILES.map(m => ({
503
+ id: m.id,
504
+ name: m.name,
505
+ description: m.description,
506
+ size: m.size
507
+ }))
508
+ })
509
+
510
+ // IPC: 下载单个模型
511
+ ipcMain.handle('download-model', async (_, modelId: string) => {
512
+ try {
513
+ await downloadModel(modelId)
514
+ return { success: true }
515
+ } catch (error) {
516
+ return {
517
+ success: false,
518
+ error: error instanceof Error ? error.message : '下载失败'
519
+ }
520
+ }
521
+ })
522
+
523
+ // IPC: 下载所有未安装的模型
524
+ ipcMain.handle('download-all-models', async () => {
525
+ const { models } = checkModelsInstalled()
526
+ const pendingModels = models.filter(m => m.status !== 'completed')
527
+
528
+ const results: { modelId: string; success: boolean; error?: string }[] = []
529
+
530
+ for (const model of pendingModels) {
531
+ try {
532
+ await downloadModel(model.id)
533
+ results.push({ modelId: model.id, success: true })
534
+ } catch (error) {
535
+ results.push({
536
+ modelId: model.id,
537
+ success: false,
538
+ error: error instanceof Error ? error.message : '下载失败'
539
+ })
540
+ }
541
+ }
542
+
543
+ return results
544
+ })
545
+
546
+ // IPC: 获取模型存储目录
547
+ ipcMain.handle('get-models-dir', () => {
548
+ return MODELS_DIR
549
+ })
550
+
551
  // IPC: 保存音色文件到本地
552
  ipcMain.handle('save-voice-files', async (_, request: SaveVoiceFilesRequest) => {
553
  try {
tts-voice-app/src/preload/index.d.ts CHANGED
@@ -49,6 +49,39 @@ interface SaveAudioResult {
49
  error?: string
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  declare global {
53
  interface Window {
54
  electron: ElectronAPI
@@ -60,6 +93,13 @@ declare global {
60
  getVoiceStorageDir: () => Promise<string>
61
  runInference: (request: InferenceRequest) => Promise<InferenceResult>
62
  saveAudioFile: (tempPath: string) => Promise<SaveAudioResult>
 
 
 
 
 
 
 
63
  }
64
  }
65
  }
 
49
  error?: string
50
  }
51
 
52
+ // Model status type
53
+ interface ModelStatus {
54
+ id: string
55
+ name: string
56
+ description: string
57
+ size?: string
58
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error'
59
+ progress: number
60
+ error?: string
61
+ }
62
+
63
+ // Model config type
64
+ interface ModelConfig {
65
+ id: string
66
+ name: string
67
+ description: string
68
+ size?: string
69
+ }
70
+
71
+ // Models check result type
72
+ interface ModelsCheckResult {
73
+ allInstalled: boolean
74
+ models: ModelStatus[]
75
+ }
76
+
77
+ // Download progress event type
78
+ interface DownloadProgressEvent {
79
+ modelId: string
80
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error'
81
+ progress: number
82
+ error?: string
83
+ }
84
+
85
  declare global {
86
  interface Window {
87
  electron: ElectronAPI
 
93
  getVoiceStorageDir: () => Promise<string>
94
  runInference: (request: InferenceRequest) => Promise<InferenceResult>
95
  saveAudioFile: (tempPath: string) => Promise<SaveAudioResult>
96
+ // Model management operations
97
+ checkModelsStatus: () => Promise<ModelsCheckResult>
98
+ getModelConfigs: () => Promise<ModelConfig[]>
99
+ downloadModel: (modelId: string) => Promise<{ success: boolean; error?: string }>
100
+ downloadAllModels: () => Promise<{ modelId: string; success: boolean; error?: string }[]>
101
+ getModelsDir: () => Promise<string>
102
+ onModelDownloadProgress: (callback: (event: DownloadProgressEvent) => void) => () => void
103
  }
104
  }
105
  }
tts-voice-app/src/preload/index.ts CHANGED
@@ -50,6 +50,39 @@ interface SaveAudioResult {
50
  error?: string
51
  }
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  // Custom APIs for renderer
54
  const api = {
55
  getApiBaseUrl: (): Promise<string> => ipcRenderer.invoke('get-api-base-url'),
@@ -70,7 +103,30 @@ const api = {
70
  ipcRenderer.invoke('run-inference', request),
71
 
72
  saveAudioFile: (tempPath: string): Promise<SaveAudioResult> =>
73
- ipcRenderer.invoke('save-audio-file', tempPath)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
 
76
  // Use `contextBridge` APIs to expose Electron APIs to
 
50
  error?: string
51
  }
52
 
53
+ // Model status type
54
+ interface ModelStatus {
55
+ id: string
56
+ name: string
57
+ description: string
58
+ size?: string
59
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error'
60
+ progress: number
61
+ error?: string
62
+ }
63
+
64
+ // Model config type
65
+ interface ModelConfig {
66
+ id: string
67
+ name: string
68
+ description: string
69
+ size?: string
70
+ }
71
+
72
+ // Models check result type
73
+ interface ModelsCheckResult {
74
+ allInstalled: boolean
75
+ models: ModelStatus[]
76
+ }
77
+
78
+ // Download progress event type
79
+ interface DownloadProgressEvent {
80
+ modelId: string
81
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error'
82
+ progress: number
83
+ error?: string
84
+ }
85
+
86
  // Custom APIs for renderer
87
  const api = {
88
  getApiBaseUrl: (): Promise<string> => ipcRenderer.invoke('get-api-base-url'),
 
103
  ipcRenderer.invoke('run-inference', request),
104
 
105
  saveAudioFile: (tempPath: string): Promise<SaveAudioResult> =>
106
+ ipcRenderer.invoke('save-audio-file', tempPath),
107
+
108
+ // Model management operations
109
+ checkModelsStatus: (): Promise<ModelsCheckResult> =>
110
+ ipcRenderer.invoke('check-models-status'),
111
+
112
+ getModelConfigs: (): Promise<ModelConfig[]> =>
113
+ ipcRenderer.invoke('get-model-configs'),
114
+
115
+ downloadModel: (modelId: string): Promise<{ success: boolean; error?: string }> =>
116
+ ipcRenderer.invoke('download-model', modelId),
117
+
118
+ downloadAllModels: (): Promise<{ modelId: string; success: boolean; error?: string }[]> =>
119
+ ipcRenderer.invoke('download-all-models'),
120
+
121
+ getModelsDir: (): Promise<string> =>
122
+ ipcRenderer.invoke('get-models-dir'),
123
+
124
+ // Model download progress listener
125
+ onModelDownloadProgress: (callback: (event: DownloadProgressEvent) => void): (() => void) => {
126
+ const handler = (_: Electron.IpcRendererEvent, data: DownloadProgressEvent) => callback(data)
127
+ ipcRenderer.on('model-download-progress', handler)
128
+ return () => ipcRenderer.removeListener('model-download-progress', handler)
129
+ }
130
  }
131
 
132
  // Use `contextBridge` APIs to expose Electron APIs to
tts-voice-app/src/renderer/src/App.vue CHANGED
@@ -1,36 +1,93 @@
1
  <script setup lang="ts">
2
- import { onMounted } from 'vue'
 
3
  import Sidebar from './components/layout/Sidebar.vue'
4
  import { useAppStore } from './stores/app'
 
5
 
6
  const appStore = useAppStore()
 
 
 
7
 
8
- onMounted(() => {
 
 
 
 
 
9
  // 初始化主题
10
  document.documentElement.setAttribute('data-theme', appStore.theme)
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  })
12
  </script>
13
 
14
  <template>
15
- <div class="app-container">
16
- <Sidebar />
17
- <main class="main-content">
 
 
 
18
  <router-view />
19
  </main>
20
  </div>
21
  </template>
22
 
23
  <style lang="scss" scoped>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  .app-container {
25
  display: flex;
26
  height: 100vh;
27
  background-color: var(--bg-primary);
28
  color: var(--text-primary);
 
 
 
 
29
  }
30
 
31
  .main-content {
32
  flex: 1;
33
  overflow: auto;
34
  padding: 24px;
 
 
 
 
35
  }
36
  </style>
 
1
  <script setup lang="ts">
2
+ import { onMounted, ref, computed } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
  import Sidebar from './components/layout/Sidebar.vue'
5
  import { useAppStore } from './stores/app'
6
+ import { useSetupStore } from './stores/setup'
7
 
8
  const appStore = useAppStore()
9
+ const setupStore = useSetupStore()
10
+ const route = useRoute()
11
+ const router = useRouter()
12
 
13
+ const isInitializing = ref(true)
14
+
15
+ // Check if we should hide the layout (e.g., on setup page)
16
+ const hideLayout = computed(() => route.meta?.hideLayout === true)
17
+
18
+ onMounted(async () => {
19
  // 初始化主题
20
  document.documentElement.setAttribute('data-theme', appStore.theme)
21
+
22
+ // Check if models are installed
23
+ try {
24
+ const allInstalled = await setupStore.checkModelsStatus()
25
+ if (!allInstalled && route.path !== '/setup') {
26
+ // Redirect to setup page if models are not installed
27
+ router.replace('/setup')
28
+ }
29
+ } catch (error) {
30
+ console.error('Failed to check models status:', error)
31
+ } finally {
32
+ isInitializing.value = false
33
+ }
34
  })
35
  </script>
36
 
37
  <template>
38
+ <div v-if="isInitializing" class="app-loading">
39
+ <div class="loading-spinner"></div>
40
+ </div>
41
+ <div v-else class="app-container" :class="{ 'no-sidebar': hideLayout }">
42
+ <Sidebar v-if="!hideLayout" />
43
+ <main class="main-content" :class="{ 'full-width': hideLayout }">
44
  <router-view />
45
  </main>
46
  </div>
47
  </template>
48
 
49
  <style lang="scss" scoped>
50
+ .app-loading {
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ height: 100vh;
55
+ background-color: var(--bg-primary);
56
+
57
+ .loading-spinner {
58
+ width: 40px;
59
+ height: 40px;
60
+ border: 3px solid var(--border-color);
61
+ border-top-color: var(--primary-color);
62
+ border-radius: 50%;
63
+ animation: spin 1s linear infinite;
64
+ }
65
+ }
66
+
67
+ @keyframes spin {
68
+ to {
69
+ transform: rotate(360deg);
70
+ }
71
+ }
72
+
73
  .app-container {
74
  display: flex;
75
  height: 100vh;
76
  background-color: var(--bg-primary);
77
  color: var(--text-primary);
78
+
79
+ &.no-sidebar {
80
+ display: block;
81
+ }
82
  }
83
 
84
  .main-content {
85
  flex: 1;
86
  overflow: auto;
87
  padding: 24px;
88
+
89
+ &.full-width {
90
+ padding: 0;
91
+ }
92
  }
93
  </style>
tts-voice-app/src/renderer/src/locales/en-US.json CHANGED
@@ -132,5 +132,24 @@
132
  "theme": {
133
  "light": "Light Mode",
134
  "dark": "Dark Mode"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
136
  }
 
132
  "theme": {
133
  "light": "Light Mode",
134
  "dark": "Dark Mode"
135
+ },
136
+ "setup": {
137
+ "title": "Welcome to TTS Voice",
138
+ "subtitle": "First-time setup requires downloading necessary model files. This may take some time.",
139
+ "modelsTitle": "Model Files",
140
+ "startDownload": "Start Download",
141
+ "retryDownload": "Retry Download",
142
+ "retryFailed": "Retry Failed",
143
+ "continue": "Get Started",
144
+ "downloadingHint": "Downloading in progress, please do not close the app...",
145
+ "overallProgress": "Overall Progress",
146
+ "storageHint": "Model files will be saved to ~/.moyoyo-tts/models",
147
+ "status": {
148
+ "pending": "Pending",
149
+ "downloading": "Downloading",
150
+ "extracting": "Extracting",
151
+ "completed": "Completed",
152
+ "error": "Failed"
153
+ }
154
  }
155
  }
tts-voice-app/src/renderer/src/locales/zh-CN.json CHANGED
@@ -132,5 +132,24 @@
132
  "theme": {
133
  "light": "浅色模式",
134
  "dark": "深色模式"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
136
  }
 
132
  "theme": {
133
  "light": "浅色模式",
134
  "dark": "深色模式"
135
+ },
136
+ "setup": {
137
+ "title": "欢迎使用 TTS Voice",
138
+ "subtitle": "首次使用需要下载必要的模型文件,这可能需要一些时间",
139
+ "modelsTitle": "模型文件",
140
+ "startDownload": "开始下载",
141
+ "retryDownload": "重新下载",
142
+ "retryFailed": "重试失败项",
143
+ "continue": "开始使用",
144
+ "downloadingHint": "正在下载,请勿关闭应用...",
145
+ "overallProgress": "总体进度",
146
+ "storageHint": "模型文件将保存在 ~/.moyoyo-tts/models 目录",
147
+ "status": {
148
+ "pending": "等待中",
149
+ "downloading": "下载中",
150
+ "extracting": "解压中",
151
+ "completed": "已完成",
152
+ "error": "下载失败"
153
+ }
154
  }
155
  }
tts-voice-app/src/renderer/src/router/index.ts CHANGED
@@ -6,6 +6,12 @@ const routes: RouteRecordRaw[] = [
6
  path: '/',
7
  redirect: '/tts'
8
  },
 
 
 
 
 
 
9
  {
10
  path: '/tts',
11
  name: 'TextToSpeech',
 
6
  path: '/',
7
  redirect: '/tts'
8
  },
9
+ {
10
+ path: '/setup',
11
+ name: 'ModelSetup',
12
+ component: () => import('@/views/ModelSetup.vue'),
13
+ meta: { title: 'Model Setup', hideLayout: true }
14
+ },
15
  {
16
  path: '/tts',
17
  name: 'TextToSpeech',
tts-voice-app/src/renderer/src/stores/setup.ts ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed, onUnmounted } from 'vue'
3
+
4
+ // Model status type
5
+ export interface ModelStatus {
6
+ id: string
7
+ name: string
8
+ description: string
9
+ size?: string
10
+ status: 'pending' | 'downloading' | 'extracting' | 'completed' | 'error'
11
+ progress: number
12
+ error?: string
13
+ }
14
+
15
+ export const useSetupStore = defineStore('setup', () => {
16
+ // State
17
+ const models = ref<ModelStatus[]>([])
18
+ const isChecking = ref(false)
19
+ const isDownloading = ref(false)
20
+ const setupCompleted = ref(false)
21
+
22
+ // Computed
23
+ const allModelsInstalled = computed(() =>
24
+ models.value.length > 0 && models.value.every(m => m.status === 'completed')
25
+ )
26
+
27
+ const downloadProgress = computed(() => {
28
+ if (models.value.length === 0) return 0
29
+ const total = models.value.reduce((sum, m) => sum + m.progress, 0)
30
+ return Math.round(total / models.value.length)
31
+ })
32
+
33
+ const hasErrors = computed(() =>
34
+ models.value.some(m => m.status === 'error')
35
+ )
36
+
37
+ const pendingModels = computed(() =>
38
+ models.value.filter(m => m.status === 'pending' || m.status === 'error')
39
+ )
40
+
41
+ // Progress listener cleanup function
42
+ let cleanupProgressListener: (() => void) | null = null
43
+
44
+ // Actions
45
+ async function checkModelsStatus(): Promise<boolean> {
46
+ isChecking.value = true
47
+ try {
48
+ const result = await window.api.checkModelsStatus()
49
+ models.value = result.models
50
+ setupCompleted.value = result.allInstalled
51
+ return result.allInstalled
52
+ } catch (error) {
53
+ console.error('Failed to check models status:', error)
54
+ return false
55
+ } finally {
56
+ isChecking.value = false
57
+ }
58
+ }
59
+
60
+ function setupProgressListener(): void {
61
+ // Clean up existing listener
62
+ if (cleanupProgressListener) {
63
+ cleanupProgressListener()
64
+ }
65
+
66
+ // Set up new listener
67
+ cleanupProgressListener = window.api.onModelDownloadProgress((event) => {
68
+ const model = models.value.find(m => m.id === event.modelId)
69
+ if (model) {
70
+ model.status = event.status
71
+ model.progress = event.progress
72
+ if (event.error) {
73
+ model.error = event.error
74
+ }
75
+ }
76
+
77
+ // Check if all downloads completed
78
+ if (models.value.every(m => m.status === 'completed')) {
79
+ isDownloading.value = false
80
+ setupCompleted.value = true
81
+ }
82
+ })
83
+ }
84
+
85
+ async function startDownload(): Promise<void> {
86
+ isDownloading.value = true
87
+
88
+ // Setup progress listener before starting download
89
+ setupProgressListener()
90
+
91
+ try {
92
+ // Download models one by one
93
+ for (const model of models.value) {
94
+ if (model.status === 'completed') continue
95
+
96
+ // Reset error state if retrying
97
+ if (model.status === 'error') {
98
+ model.error = undefined
99
+ }
100
+
101
+ await window.api.downloadModel(model.id)
102
+ }
103
+ } catch (error) {
104
+ console.error('Download failed:', error)
105
+ } finally {
106
+ // Check final status
107
+ const allDone = models.value.every(m => m.status === 'completed')
108
+ if (allDone) {
109
+ setupCompleted.value = true
110
+ }
111
+ isDownloading.value = false
112
+ }
113
+ }
114
+
115
+ async function retryFailedDownloads(): Promise<void> {
116
+ const failedModels = models.value.filter(m => m.status === 'error')
117
+ if (failedModels.length === 0) return
118
+
119
+ isDownloading.value = true
120
+ setupProgressListener()
121
+
122
+ try {
123
+ for (const model of failedModels) {
124
+ model.status = 'pending'
125
+ model.progress = 0
126
+ model.error = undefined
127
+ await window.api.downloadModel(model.id)
128
+ }
129
+ } catch (error) {
130
+ console.error('Retry download failed:', error)
131
+ } finally {
132
+ const allDone = models.value.every(m => m.status === 'completed')
133
+ if (allDone) {
134
+ setupCompleted.value = true
135
+ }
136
+ isDownloading.value = false
137
+ }
138
+ }
139
+
140
+ function cleanup(): void {
141
+ if (cleanupProgressListener) {
142
+ cleanupProgressListener()
143
+ cleanupProgressListener = null
144
+ }
145
+ }
146
+
147
+ // Auto cleanup on unmount
148
+ onUnmounted(() => {
149
+ cleanup()
150
+ })
151
+
152
+ return {
153
+ // State
154
+ models,
155
+ isChecking,
156
+ isDownloading,
157
+ setupCompleted,
158
+
159
+ // Computed
160
+ allModelsInstalled,
161
+ downloadProgress,
162
+ hasErrors,
163
+ pendingModels,
164
+
165
+ // Actions
166
+ checkModelsStatus,
167
+ startDownload,
168
+ retryFailedDownloads,
169
+ cleanup
170
+ }
171
+ })
tts-voice-app/src/renderer/src/views/ModelSetup.vue ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { onMounted, computed } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { useSetupStore } from '@/stores/setup'
5
+ import { useI18n } from 'vue-i18n'
6
+ import { Download, RefreshRight, ArrowRight, Loading, CircleCheck, CircleClose, Clock } from '@element-plus/icons-vue'
7
+
8
+ const router = useRouter()
9
+ const setupStore = useSetupStore()
10
+ const { t } = useI18n()
11
+
12
+ // Status icon mapping
13
+ const statusIcons = {
14
+ pending: 'el-icon-time',
15
+ downloading: 'el-icon-loading',
16
+ extracting: 'el-icon-loading',
17
+ completed: 'el-icon-check',
18
+ error: 'el-icon-close'
19
+ }
20
+
21
+ // Status color mapping
22
+ const statusColors = computed(() => ({
23
+ pending: 'var(--text-muted)',
24
+ downloading: 'var(--primary-color)',
25
+ extracting: 'var(--warning-color)',
26
+ completed: 'var(--success-color)',
27
+ error: 'var(--danger-color)'
28
+ }))
29
+
30
+ // Get status text
31
+ function getStatusText(status: string): string {
32
+ const statusMap: Record<string, string> = {
33
+ pending: t('setup.status.pending'),
34
+ downloading: t('setup.status.downloading'),
35
+ extracting: t('setup.status.extracting'),
36
+ completed: t('setup.status.completed'),
37
+ error: t('setup.status.error')
38
+ }
39
+ return statusMap[status] || status
40
+ }
41
+
42
+ // Handle start download
43
+ async function handleStartDownload(): Promise<void> {
44
+ await setupStore.startDownload()
45
+ }
46
+
47
+ // Handle retry failed
48
+ async function handleRetry(): Promise<void> {
49
+ await setupStore.retryFailedDownloads()
50
+ }
51
+
52
+ // Handle continue to app
53
+ function handleContinue(): void {
54
+ router.replace('/tts')
55
+ }
56
+
57
+ // Check models on mount
58
+ onMounted(async () => {
59
+ const allInstalled = await setupStore.checkModelsStatus()
60
+ if (allInstalled) {
61
+ // All models already installed, redirect to main app
62
+ router.replace('/tts')
63
+ }
64
+ })
65
+ </script>
66
+
67
+ <template>
68
+ <div class="setup-container">
69
+ <!-- Background decoration -->
70
+ <div class="bg-decoration">
71
+ <div class="circle circle-1"></div>
72
+ <div class="circle circle-2"></div>
73
+ <div class="circle circle-3"></div>
74
+ </div>
75
+
76
+ <!-- Main content -->
77
+ <div class="setup-content">
78
+ <!-- Header -->
79
+ <div class="setup-header">
80
+ <div class="logo">
81
+ <svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
82
+ <rect width="48" height="48" rx="12" fill="url(#logo-gradient)"/>
83
+ <path d="M14 24L22 32L34 16" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
84
+ <defs>
85
+ <linearGradient id="logo-gradient" x1="0" y1="0" x2="48" y2="48">
86
+ <stop stop-color="#818cf8"/>
87
+ <stop offset="1" stop-color="#6366f1"/>
88
+ </linearGradient>
89
+ </defs>
90
+ </svg>
91
+ </div>
92
+ <h1 class="title">{{ t('setup.title') }}</h1>
93
+ <p class="subtitle">{{ t('setup.subtitle') }}</p>
94
+ </div>
95
+
96
+ <!-- Models list -->
97
+ <div class="models-card">
98
+ <div class="models-header">
99
+ <h2>{{ t('setup.modelsTitle') }}</h2>
100
+ <span class="models-count">
101
+ {{ setupStore.models.filter(m => m.status === 'completed').length }} / {{ setupStore.models.length }}
102
+ </span>
103
+ </div>
104
+
105
+ <div class="models-list">
106
+ <div
107
+ v-for="model in setupStore.models"
108
+ :key="model.id"
109
+ class="model-item"
110
+ :class="{ 'is-error': model.status === 'error' }"
111
+ >
112
+ <div class="model-info">
113
+ <div class="model-status">
114
+ <el-icon
115
+ :class="{ 'is-loading': model.status === 'downloading' || model.status === 'extracting' }"
116
+ :style="{ color: statusColors[model.status] }"
117
+ >
118
+ <component
119
+ :is="model.status === 'completed' ? 'CircleCheck' :
120
+ model.status === 'error' ? 'CircleClose' :
121
+ model.status === 'downloading' || model.status === 'extracting' ? 'Loading' : 'Clock'"
122
+ />
123
+ </el-icon>
124
+ </div>
125
+ <div class="model-details">
126
+ <div class="model-name">{{ model.name }}</div>
127
+ <div class="model-desc">{{ model.description }}</div>
128
+ <div v-if="model.error" class="model-error">{{ model.error }}</div>
129
+ </div>
130
+ <div class="model-size">{{ model.size }}</div>
131
+ </div>
132
+
133
+ <div
134
+ v-if="model.status === 'downloading' || model.status === 'extracting'"
135
+ class="model-progress"
136
+ >
137
+ <el-progress
138
+ :percentage="model.progress"
139
+ :status="model.status === 'extracting' ? 'warning' : undefined"
140
+ :stroke-width="6"
141
+ :show-text="false"
142
+ />
143
+ <span class="progress-text">
144
+ {{ getStatusText(model.status) }} {{ model.progress }}%
145
+ </span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Overall progress -->
151
+ <div v-if="setupStore.isDownloading" class="overall-progress">
152
+ <div class="progress-label">
153
+ <span>{{ t('setup.overallProgress') }}</span>
154
+ <span>{{ setupStore.downloadProgress }}%</span>
155
+ </div>
156
+ <el-progress
157
+ :percentage="setupStore.downloadProgress"
158
+ :stroke-width="8"
159
+ :show-text="false"
160
+ color="var(--primary-color)"
161
+ />
162
+ </div>
163
+ </div>
164
+
165
+ <!-- Actions -->
166
+ <div class="setup-actions">
167
+ <el-button
168
+ v-if="!setupStore.allModelsInstalled && !setupStore.isDownloading"
169
+ type="primary"
170
+ size="large"
171
+ :loading="setupStore.isChecking"
172
+ @click="handleStartDownload"
173
+ >
174
+ <el-icon v-if="!setupStore.isChecking" class="btn-icon"><Download /></el-icon>
175
+ {{ setupStore.hasErrors ? t('setup.retryDownload') : t('setup.startDownload') }}
176
+ </el-button>
177
+
178
+ <el-button
179
+ v-if="setupStore.hasErrors && !setupStore.isDownloading"
180
+ type="warning"
181
+ size="large"
182
+ @click="handleRetry"
183
+ >
184
+ <el-icon class="btn-icon"><RefreshRight /></el-icon>
185
+ {{ t('setup.retryFailed') }}
186
+ </el-button>
187
+
188
+ <el-button
189
+ v-if="setupStore.allModelsInstalled"
190
+ type="primary"
191
+ size="large"
192
+ @click="handleContinue"
193
+ >
194
+ <el-icon class="btn-icon"><ArrowRight /></el-icon>
195
+ {{ t('setup.continue') }}
196
+ </el-button>
197
+
198
+ <div v-if="setupStore.isDownloading" class="downloading-hint">
199
+ <el-icon class="is-loading"><Loading /></el-icon>
200
+ <span>{{ t('setup.downloadingHint') }}</span>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Footer -->
205
+ <div class="setup-footer">
206
+ <p>{{ t('setup.storageHint') }}</p>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </template>
211
+
212
+ <style lang="scss" scoped>
213
+ .setup-container {
214
+ position: fixed;
215
+ top: 0;
216
+ left: 0;
217
+ right: 0;
218
+ bottom: 0;
219
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #1a1a2e 100%);
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ overflow: auto;
224
+ padding: 40px 20px;
225
+ }
226
+
227
+ .bg-decoration {
228
+ position: absolute;
229
+ top: 0;
230
+ left: 0;
231
+ right: 0;
232
+ bottom: 0;
233
+ overflow: hidden;
234
+ pointer-events: none;
235
+
236
+ .circle {
237
+ position: absolute;
238
+ border-radius: 50%;
239
+ opacity: 0.1;
240
+ }
241
+
242
+ .circle-1 {
243
+ width: 600px;
244
+ height: 600px;
245
+ background: linear-gradient(135deg, #6366f1, #818cf8);
246
+ top: -200px;
247
+ right: -100px;
248
+ }
249
+
250
+ .circle-2 {
251
+ width: 400px;
252
+ height: 400px;
253
+ background: linear-gradient(135deg, #10b981, #34d399);
254
+ bottom: -100px;
255
+ left: -100px;
256
+ }
257
+
258
+ .circle-3 {
259
+ width: 300px;
260
+ height: 300px;
261
+ background: linear-gradient(135deg, #f59e0b, #fbbf24);
262
+ top: 50%;
263
+ left: 50%;
264
+ transform: translate(-50%, -50%);
265
+ }
266
+ }
267
+
268
+ .setup-content {
269
+ position: relative;
270
+ z-index: 1;
271
+ width: 100%;
272
+ max-width: 640px;
273
+ }
274
+
275
+ .setup-header {
276
+ text-align: center;
277
+ margin-bottom: 40px;
278
+
279
+ .logo {
280
+ width: 72px;
281
+ height: 72px;
282
+ margin: 0 auto 24px;
283
+
284
+ svg {
285
+ width: 100%;
286
+ height: 100%;
287
+ filter: drop-shadow(0 8px 24px rgba(99, 102, 241, 0.4));
288
+ }
289
+ }
290
+
291
+ .title {
292
+ font-size: 32px;
293
+ font-weight: 700;
294
+ color: #ffffff;
295
+ margin: 0 0 12px;
296
+ letter-spacing: -0.5px;
297
+ }
298
+
299
+ .subtitle {
300
+ font-size: 16px;
301
+ color: rgba(255, 255, 255, 0.6);
302
+ margin: 0;
303
+ line-height: 1.6;
304
+ }
305
+ }
306
+
307
+ .models-card {
308
+ background: rgba(255, 255, 255, 0.05);
309
+ backdrop-filter: blur(20px);
310
+ border: 1px solid rgba(255, 255, 255, 0.1);
311
+ border-radius: 20px;
312
+ padding: 28px;
313
+ margin-bottom: 32px;
314
+
315
+ .models-header {
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: space-between;
319
+ margin-bottom: 20px;
320
+
321
+ h2 {
322
+ font-size: 18px;
323
+ font-weight: 600;
324
+ color: #ffffff;
325
+ margin: 0;
326
+ }
327
+
328
+ .models-count {
329
+ font-size: 14px;
330
+ color: rgba(255, 255, 255, 0.5);
331
+ background: rgba(255, 255, 255, 0.1);
332
+ padding: 4px 12px;
333
+ border-radius: 12px;
334
+ }
335
+ }
336
+ }
337
+
338
+ .models-list {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 12px;
342
+ }
343
+
344
+ .model-item {
345
+ background: rgba(255, 255, 255, 0.03);
346
+ border: 1px solid rgba(255, 255, 255, 0.06);
347
+ border-radius: 12px;
348
+ padding: 16px;
349
+ transition: all 0.3s ease;
350
+
351
+ &:hover {
352
+ background: rgba(255, 255, 255, 0.05);
353
+ border-color: rgba(255, 255, 255, 0.1);
354
+ }
355
+
356
+ &.is-error {
357
+ border-color: rgba(239, 68, 68, 0.3);
358
+ background: rgba(239, 68, 68, 0.05);
359
+ }
360
+
361
+ .model-info {
362
+ display: flex;
363
+ align-items: flex-start;
364
+ gap: 12px;
365
+ }
366
+
367
+ .model-status {
368
+ flex-shrink: 0;
369
+ width: 32px;
370
+ height: 32px;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ background: rgba(255, 255, 255, 0.05);
375
+ border-radius: 8px;
376
+
377
+ .el-icon {
378
+ font-size: 18px;
379
+
380
+ &.is-loading {
381
+ animation: rotate 1s linear infinite;
382
+ }
383
+ }
384
+ }
385
+
386
+ .model-details {
387
+ flex: 1;
388
+ min-width: 0;
389
+ }
390
+
391
+ .model-name {
392
+ font-size: 15px;
393
+ font-weight: 500;
394
+ color: #ffffff;
395
+ margin-bottom: 4px;
396
+ }
397
+
398
+ .model-desc {
399
+ font-size: 13px;
400
+ color: rgba(255, 255, 255, 0.5);
401
+ line-height: 1.4;
402
+ }
403
+
404
+ .model-error {
405
+ font-size: 12px;
406
+ color: var(--danger-color);
407
+ margin-top: 6px;
408
+ }
409
+
410
+ .model-size {
411
+ flex-shrink: 0;
412
+ font-size: 13px;
413
+ color: rgba(255, 255, 255, 0.4);
414
+ padding: 4px 8px;
415
+ background: rgba(255, 255, 255, 0.05);
416
+ border-radius: 6px;
417
+ }
418
+
419
+ .model-progress {
420
+ margin-top: 12px;
421
+
422
+ .progress-text {
423
+ display: block;
424
+ font-size: 12px;
425
+ color: rgba(255, 255, 255, 0.5);
426
+ margin-top: 6px;
427
+ text-align: right;
428
+ }
429
+ }
430
+ }
431
+
432
+ .overall-progress {
433
+ margin-top: 20px;
434
+ padding-top: 20px;
435
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
436
+
437
+ .progress-label {
438
+ display: flex;
439
+ justify-content: space-between;
440
+ font-size: 14px;
441
+ color: rgba(255, 255, 255, 0.7);
442
+ margin-bottom: 10px;
443
+ }
444
+ }
445
+
446
+ .setup-actions {
447
+ display: flex;
448
+ flex-direction: column;
449
+ align-items: center;
450
+ gap: 16px;
451
+
452
+ .el-button {
453
+ min-width: 200px;
454
+ height: 48px;
455
+ font-size: 16px;
456
+ font-weight: 500;
457
+ border-radius: 12px;
458
+
459
+ &--primary {
460
+ background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%);
461
+ border: none;
462
+ box-shadow: 0 8px 24px rgba(99, 102, 241, 0.3);
463
+
464
+ &:hover {
465
+ background: linear-gradient(135deg, #818cf8 0%, #a5b4fc 100%);
466
+ box-shadow: 0 8px 32px rgba(99, 102, 241, 0.4);
467
+ }
468
+ }
469
+
470
+ &--warning {
471
+ background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
472
+ border: none;
473
+ color: #1a1a2e;
474
+ }
475
+ }
476
+
477
+ .btn-icon {
478
+ margin-right: 8px;
479
+ }
480
+
481
+ .downloading-hint {
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 8px;
485
+ font-size: 14px;
486
+ color: rgba(255, 255, 255, 0.5);
487
+
488
+ .el-icon {
489
+ color: var(--primary-color);
490
+ }
491
+ }
492
+ }
493
+
494
+ .setup-footer {
495
+ margin-top: 32px;
496
+ text-align: center;
497
+
498
+ p {
499
+ font-size: 13px;
500
+ color: rgba(255, 255, 255, 0.4);
501
+ margin: 0;
502
+ }
503
+ }
504
+
505
+ @keyframes rotate {
506
+ from {
507
+ transform: rotate(0deg);
508
+ }
509
+ to {
510
+ transform: rotate(360deg);
511
+ }
512
+ }
513
+
514
+ // Element Plus overrides for dark theme
515
+ :deep(.el-progress-bar__outer) {
516
+ background-color: rgba(255, 255, 255, 0.1);
517
+ }
518
+
519
+ :deep(.el-progress-bar__inner) {
520
+ background: linear-gradient(90deg, #6366f1, #818cf8);
521
+ }
522
+
523
+ :deep(.el-progress--warning .el-progress-bar__inner) {
524
+ background: linear-gradient(90deg, #f59e0b, #fbbf24);
525
+ }
526
+ </style>