diff --git a/package-lock.json b/package-lock.json
index b6d6b337..dfd9fd78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "Duix.Avatar",
- "version": "1.0.4",
+ "version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Duix.Avatar",
- "version": "1.0.4",
+ "version": "1.0.5",
"hasInstallScript": true,
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
@@ -21,8 +21,10 @@
"pinia": "^2.2.6",
"tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3",
+ "tiktok-live-connector": "^2.0.7-beta1",
"vue-i18n": "^10.0.5",
- "vue-router": "^4.4.5"
+ "vue-router": "^4.4.5",
+ "ws": "^8.18.3"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
@@ -319,6 +321,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@bufbuild/protobuf": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz",
+ "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==",
+ "license": "(Apache-2.0 AND BSD-3-Clause)"
+ },
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -384,9 +392,9 @@
}
},
"node_modules/@electron/asar/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -575,9 +583,9 @@
}
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1082,9 +1090,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1144,6 +1152,18 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@eulerstream/euler-api-sdk": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@eulerstream/euler-api-sdk/-/euler-api-sdk-0.1.2.tgz",
+ "integrity": "sha512-S9ktZAUhAbqMlVJGN+dGgVo6QVG6EdIFiRYTWp0x6nQBIBZAyLUpz62XEXeeOrUwL0UiPUagu5wNtRoZC7oFjg==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -1161,9 +1181,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1207,13 +1227,13 @@
"license": "BSD-3-Clause"
},
"node_modules/@intlify/core-base": {
- "version": "10.0.6",
- "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
- "integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
+ "integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
"license": "MIT",
"dependencies": {
- "@intlify/message-compiler": "10.0.6",
- "@intlify/shared": "10.0.6"
+ "@intlify/message-compiler": "10.0.8",
+ "@intlify/shared": "10.0.8"
},
"engines": {
"node": ">= 16"
@@ -1223,12 +1243,12 @@
}
},
"node_modules/@intlify/message-compiler": {
- "version": "10.0.6",
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
- "integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
+ "integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
"license": "MIT",
"dependencies": {
- "@intlify/shared": "10.0.6",
+ "@intlify/shared": "10.0.8",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -1239,9 +1259,9 @@
}
},
"node_modules/@intlify/shared": {
- "version": "10.0.6",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
- "integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
+ "integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -1567,6 +1587,70 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.36.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
@@ -1974,6 +2058,12 @@
"@types/lodash": "*"
}
},
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "license": "MIT"
+ },
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -2037,6 +2127,15 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -2774,13 +2873,13 @@
}
},
"node_modules/axios": {
- "version": "1.8.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
- "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -2885,9 +2984,9 @@
"optional": true
},
"node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3107,6 +3206,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/callable-instance": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz",
+ "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==",
+ "license": "MIT"
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3627,9 +3732,9 @@
}
},
"node_modules/dir-compare/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3801,9 +3906,9 @@
}
},
"node_modules/electron": {
- "version": "33.4.6",
- "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.6.tgz",
- "integrity": "sha512-INNAYGkMWOb10FNMHaSdow0ekFiftFznpn6Cqo/vEgEapBMyNzGJa+0IVCezmgyRUW08FCHd+ngjvynALSndxQ==",
+ "version": "33.4.11",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
+ "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -4516,9 +4621,9 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4890,14 +4995,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -5110,9 +5216,9 @@
"peer": true
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5636,9 +5742,9 @@
}
},
"node_modules/jake/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6016,6 +6122,12 @@
"license": "MIT",
"peer": true
},
+ "node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "license": "Apache-2.0"
+ },
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -6732,6 +6844,32 @@
"node": ">=10"
}
},
+ "node_modules/protobufjs": {
+ "version": "6.11.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
+ "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.1",
+ "@types/node": ">=13.7.0",
+ "long": "^4.0.0"
+ },
+ "bin": {
+ "pbjs": "bin/pbjs",
+ "pbts": "bin/pbts"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -7072,6 +7210,16 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -7517,9 +7665,9 @@
}
},
"node_modules/tar-fs": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
- "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
@@ -7782,6 +7930,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tiktok-live-connector": {
+ "version": "2.0.7-beta1",
+ "resolved": "https://registry.npmjs.org/tiktok-live-connector/-/tiktok-live-connector-2.0.7-beta1.tgz",
+ "integrity": "sha512-w6nXbjAsALa5gYkMsADd2VCnsQ+yltyl8nWpTzBAQ1gwipUOMekeNWA3G5R2oK94i6jMlY/zHB+NbNt/dPPB5w==",
+ "funding": [
+ "https://buymeacoffee.com/isaackogan",
+ "https://buymeacoffee.com/zerody"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@bufbuild/protobuf": "^2.2.5",
+ "@eulerstream/euler-api-sdk": "^0.1.2",
+ "@types/ws": "^8.18.1",
+ "axios": "^1.9.0",
+ "callable-instance": "^2.0.0",
+ "protobufjs": "^6.11.2",
+ "typed-emitter": "^2.1.0",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
@@ -7795,9 +7966,9 @@
"license": "MIT"
},
"node_modules/tmp": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
- "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7828,7 +7999,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
+ "devOptional": true,
"license": "0BSD"
},
"node_modules/tunnel-agent": {
@@ -7869,6 +8040,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/typed-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
+ "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
+ "license": "MIT",
+ "optionalDependencies": {
+ "rxjs": "*"
+ }
+ },
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
@@ -7978,9 +8158,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.14",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
- "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
+ "version": "5.4.20",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
+ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8123,13 +8303,13 @@
}
},
"node_modules/vue-i18n": {
- "version": "10.0.6",
- "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
- "integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
+ "integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
"license": "MIT",
"dependencies": {
- "@intlify/core-base": "10.0.6",
- "@intlify/shared": "10.0.6",
+ "@intlify/core-base": "10.0.8",
+ "@intlify/shared": "10.0.8",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
@@ -8387,6 +8567,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
diff --git a/package.json b/package.json
index bc426eb7..443f7713 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,10 @@
"pinia": "^2.2.6",
"tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3",
+ "tiktok-live-connector": "^2.0.7-beta1",
"vue-i18n": "^10.0.5",
- "vue-router": "^4.4.5"
+ "vue-router": "^4.4.5",
+ "ws": "^8.18.3"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
diff --git a/resources/placeholder.mp4 b/resources/placeholder.mp4
new file mode 100644
index 00000000..9d0f1cf5
Binary files /dev/null and b/resources/placeholder.mp4 differ
diff --git a/src/main/api/tts.js b/src/main/api/tts.js
index 45013f8f..e9578aa4 100644
--- a/src/main/api/tts.js
+++ b/src/main/api/tts.js
@@ -2,10 +2,11 @@ import request from './request.js'
import { serviceUrl } from '../config/config.js'
import log from '../logger.js'
-export function makeAudio(param) {
+export function makeAudio(param, config = {}) {
log.debug('~ makeAudio ~ param:', JSON.stringify(param))
return request.post(`${serviceUrl.tts}/v1/invoke`, param, {
- responseType: 'arraybuffer'
+ responseType: 'arraybuffer',
+ ...config
})
}
diff --git a/src/main/dao/f2f-model.js b/src/main/dao/f2f-model.js
index 3d7e389a..0efc6620 100644
--- a/src/main/dao/f2f-model.js
+++ b/src/main/dao/f2f-model.js
@@ -1,11 +1,11 @@
import { connect } from '../db/index.js'
-export function insert({ modelName, videoPath, audioPath, voiceId }) {
+export function insert({ modelName, videoPath, audioPath, voiceId, isImage, coverPath }) {
const db = connect()
const stmt = db.prepare(
- 'INSERT INTO f2f_model (name, video_path, audio_path, voice_id, created_at) VALUES (?, ?, ?, ?, ?)'
+ 'INSERT INTO f2f_model (name, video_path, audio_path, voice_id, is_image, cover_path, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
)
- const info = stmt.run(modelName, videoPath, audioPath, voiceId, Date.now())
+ const info = stmt.run(modelName, videoPath, audioPath, voiceId, isImage, coverPath, Date.now())
return info.lastInsertRowid
}
diff --git a/src/main/db/sql.js b/src/main/db/sql.js
index e1edfdd8..877f05df 100644
--- a/src/main/db/sql.js
+++ b/src/main/db/sql.js
@@ -65,5 +65,14 @@ export default [
script: `alter table video
add voice_id integer;
`
+ },
+ {
+ version: 4,
+ script: `
+ alter table f2f_model
+ add is_image integer default 0;
+ alter table f2f_model
+ add cover_path text;
+ `
}
]
diff --git a/src/main/index.js b/src/main/index.js
index 78f37f30..740838f8 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -6,6 +6,7 @@ import { initDB } from './db/index.js'
import { registerHandler } from './service/index.js'
import { init as initInterval } from './interval/interval.js'
import { registerWebHandles } from './handlers'
+import { init as initRealtime } from './service/realtime.js'
initDB()
initInterval()
@@ -65,6 +66,7 @@ app.whenReady().then(() => {
// 注册主进程服务
registerHandler()
+ initRealtime()
const mainWindow = createWindow()
diff --git a/src/main/service/model.js b/src/main/service/model.js
index eeeb0d50..82f0f745 100644
--- a/src/main/service/model.js
+++ b/src/main/service/model.js
@@ -16,44 +16,55 @@ const MODEL_NAME = 'model'
* @param {string} videoPath 模特视频路径
* @returns
*/
-async function addModel(modelName, videoPath) {
+async function addModel(modelName, filePath, isImage = false) {
if (!fs.existsSync(assetPath.model)) {
fs.mkdirSync(assetPath.model, {
recursive: true
})
}
- // copy video to model video path
- const extname = path.extname(videoPath)
- const modelFileName = dayjs().format('YYYYMMDDHHmmssSSS') + extname
- const modelPath = path.join(assetPath.model, modelFileName)
- await toH264(videoPath, modelPath)
+ if (isImage) {
+ const extname = path.extname(filePath)
+ const modelFileName = dayjs().format('YYYYMMDDHHmmssSSS') + extname
+ const modelPath = path.join(assetPath.model, modelFileName)
+ fs.copyFileSync(filePath, modelPath)
+ const relativeModelPath = path.relative(assetPath.model, modelPath)
+ const id = insert({ modelName, coverPath: relativeModelPath, isImage: 1 })
+ return id
+ } else {
+ // copy video to model video path
+ const extname = path.extname(filePath)
+ const modelFileName = dayjs().format('YYYYMMDDHHmmssSSS') + extname
+ const modelPath = path.join(assetPath.model, modelFileName)
- // 用ffmpeg分离音频
- if (!fs.existsSync(assetPath.ttsTrain)) {
- fs.mkdirSync(assetPath.ttsTrain, {
- recursive: true
- })
- }
- const audioPath = path.join(assetPath.ttsTrain, modelFileName.replace(extname, '.wav'))
- return extractAudio(modelPath, audioPath).then(() => {
- // 训练语音模型
- const relativeAudioPath = path.relative(assetPath.ttsRoot, audioPath)
- if (process.env.NODE_ENV === 'development') {
- // TODO 写死调试
- return trainVoice('origin_audio/test.wav', 'zh')
- } else {
- return trainVoice(relativeAudioPath, 'zh')
+ await toH264(filePath, modelPath)
+
+ // 用ffmpeg分离音频
+ if (!fs.existsSync(assetPath.ttsTrain)) {
+ fs.mkdirSync(assetPath.ttsTrain, {
+ recursive: true
+ })
}
- }).then((voiceId)=>{
- // 插入模特信息
- const relativeModelPath = path.relative(assetPath.model, modelPath)
- const relativeAudioPath = path.relative(assetPath.ttsRoot, audioPath)
+ const audioPath = path.join(assetPath.ttsTrain, modelFileName.replace(extname, '.wav'))
+ return extractAudio(modelPath, audioPath).then(() => {
+ // 训练语音模型
+ const relativeAudioPath = path.relative(assetPath.ttsRoot, audioPath)
+ if (process.env.NODE_ENV === 'development') {
+ // TODO 写死调试
+ return trainVoice('origin_audio/test.wav', 'zh')
+ } else {
+ return trainVoice(relativeAudioPath, 'zh')
+ }
+ }).then((voiceId)=>{
+ // 插入模特信息
+ const relativeModelPath = path.relative(assetPath.model, modelPath)
+ const relativeAudioPath = path.relative(assetPath.ttsRoot, audioPath)
- // insert model info to db
- const id = insert({ modelName, videoPath: relativeModelPath, audioPath: relativeAudioPath, voiceId })
- return id
- })
+ // insert model info to db
+ const id = insert({ modelName, videoPath: relativeModelPath, audioPath: relativeAudioPath, voiceId, isImage: 0 })
+ return id
+ })
+ }
}
function page({ page, pageSize, name = '' }) {
@@ -101,8 +112,8 @@ function countModel(name = '') {
}
export function init() {
- ipcMain.handle(MODEL_NAME + '/addModel', (event, ...args) => {
- return addModel(...args)
+ ipcMain.handle(MODEL_NAME + '/addModel', (event, modelName, filePath, isImage) => {
+ return addModel(modelName, filePath, isImage)
})
ipcMain.handle(MODEL_NAME + '/page', (event, ...args) => {
return page(...args)
diff --git a/src/main/service/realtime.js b/src/main/service/realtime.js
new file mode 100644
index 00000000..ee1ffe43
--- /dev/null
+++ b/src/main/service/realtime.js
@@ -0,0 +1,248 @@
+import { WebSocketServer } from 'ws'
+import log from '../logger.js'
+import { makeAudio as makeAudioApi } from '../api/tts.js'
+import { makeVideo as makeVideoApi, getVideoStatus } from '../api/f2f.js'
+import { selectByID as selectF2FModelByID } from '../dao/f2f-model.js'
+import { selectByID as selectVoiceByID } from '../dao/voice.js'
+import crypto from 'crypto'
+import fs from 'fs'
+import path from 'path'
+import { assetPath } from '../config/config.js'
+import ffmpeg from 'fluent-ffmpeg'
+import { connectToTiktok } from './tiktok.js'
+
+const MODEL_NAME = 'realtime'
+const CHUNK_DURATION = 1 // seconds
+
+function init() {
+ const wss = new WebSocketServer({ port: 8080 })
+
+ wss.on('connection', function connection(ws) {
+ let textQueue = []
+ let isProcessing = false
+ let model, voice
+ let isRecording = false
+ let recordingDir = null
+ let recordedVideoPath = null
+
+ log.info('Real-time service: client connected')
+
+ ws.on('message', async function message(data) {
+ try {
+ const message = JSON.parse(data)
+ log.info('Real-time service: received message:', message)
+
+ if (message.type === 'start') {
+ model = selectF2FModelByID(message.data.modelId)
+ voice = selectVoiceByID(message.data.voiceId)
+ textQueue.push(message.data.text)
+ processTextQueue()
+ } else if (message.type === 'start-tiktok-stream') {
+ if (recordedVideoPath) {
+ const { serverUrl, streamKey } = message.data
+ startTiktokStream(serverUrl, streamKey, ws, recordedVideoPath)
+ } else {
+ ws.send(JSON.stringify({ type: 'error', data: 'No recorded video to stream.' }))
+ }
+ } else if (message.type === 'start-tiktok-comment-listener') {
+ connectToTiktok(message.data.username, (comment) => {
+ textQueue.push(comment)
+ processTextQueue()
+ })
+ } else if (message.type === 'start-recording') {
+ isRecording = true
+ recordingDir = path.join(assetPath.model, 'recordings', crypto.randomUUID())
+ fs.mkdirSync(recordingDir, { recursive: true })
+ } else if (message.type === 'stop-recording') {
+ isRecording = false
+ recordedVideoPath = await concatenateRecordedChunks(recordingDir)
+ ws.send(JSON.stringify({ type: 'recording-finished', data: { path: recordedVideoPath } }))
+ }
+ } catch (error) {
+ log.error('Real-time service error:', error)
+ ws.send(JSON.stringify({ type: 'error', data: error.message }))
+ }
+ })
+
+ async function processTextQueue() {
+ if (isProcessing || textQueue.length === 0) {
+ return
+ }
+ isProcessing = true
+ const text = textQueue.shift()
+ const audioStream = await getAudioStream(voice, text)
+ await processAudioStream(audioStream, model, ws, isRecording, recordingDir)
+ isProcessing = false
+ processTextQueue()
+ }
+
+ ws.send(JSON.stringify({ type: 'connected' }))
+ })
+
+ log.info('Real-time service initialized on port 8080')
+}
+
+async function getAudioStream(voice, text) {
+ const uuid = crypto.randomUUID()
+ const response = await makeAudioApi({
+ speaker: uuid,
+ text,
+ format: 'wav',
+ topP: 0.7,
+ max_new_tokens: 1024,
+ chunk_length: 100,
+ repetition_penalty: 1.2,
+ temperature: 0.7,
+ need_asr: false,
+ streaming: true,
+ is_fixed_seed: 0,
+ is_norm: 1,
+ reference_audio: voice.asr_format_audio_url,
+ reference_text: voice.reference_audio_text
+ }, { responseType: 'stream' })
+
+ return response
+}
+
+function processAudioStream(audioStream, model, ws, isRecording, recordingDir) {
+ return new Promise((resolve, reject) => {
+ const chunkDir = path.join(assetPath.ttsProduct, 'chunks', crypto.randomUUID())
+ fs.mkdirSync(chunkDir, { recursive: true })
+
+ const ffmpegProc = ffmpeg(audioStream)
+ .toFormat('wav')
+ .outputOptions([
+ '-f', 'segment',
+ '-segment_time', `${CHUNK_DURATION}`,
+ '-c', 'copy'
+ ])
+ .on('end', () => {
+ log.info('ffmpeg chunking finished')
+ ws.send(JSON.stringify({ type: 'end-of-stream' }))
+ fs.rm(chunkDir, { recursive: true, force: true }, () => {})
+ resolve()
+ })
+ .on('error', (err) => {
+ log.error('ffmpeg error:', err)
+ reject(err)
+ })
+ .save(path.join(chunkDir, 'chunk-%03d.wav'))
+
+ const processedChunks = new Set()
+ const watcher = fs.watch(chunkDir, async (eventType, filename) => {
+ if (filename && filename.endsWith('.wav') && !processedChunks.has(filename)) {
+ processedChunks.add(filename)
+ const audioPath = path.join(chunkDir, filename)
+ generateVideoChunk(audioPath, model, ws, isRecording, recordingDir)
+ }
+ })
+ })
+}
+
+async function generateVideoChunk(audioPath, model, ws, isRecording, recordingDir) {
+ const videoResult = await submitVideoTask(audioPath, model)
+ if (videoResult && videoResult.code === 10000) {
+ const videoPath = await pollForVideo(videoResult.data.code)
+ if (videoPath) {
+ const fullVideoPath = path.join(assetPath.model, videoPath)
+ const videoData = fs.readFileSync(fullVideoPath)
+ ws.send(videoData)
+
+ if (isRecording) {
+ fs.copyFileSync(fullVideoPath, path.join(recordingDir, path.basename(videoPath)))
+ }
+
+ fs.unlinkSync(fullVideoPath)
+ fs.unlinkSync(audioPath)
+ }
+ }
+}
+
+async function concatenateRecordedChunks(recordingDir) {
+ return new Promise((resolve, reject) => {
+ const chunkPaths = fs.readdirSync(recordingDir).map(p => path.join(recordingDir, p))
+ const fullVideoPath = path.join(assetPath.model, `recorded-video-${crypto.randomUUID()}.mp4`)
+ const ffmpegCmd = ffmpeg()
+
+ chunkPaths.forEach(p => {
+ ffmpegCmd.input(p)
+ })
+
+ ffmpegCmd
+ .on('end', () => {
+ log.info('Concatenation finished')
+ fs.rm(recordingDir, { recursive: true, force: true }, () => {})
+ resolve(fullVideoPath)
+ })
+ .on('error', (err) => {
+ log.error('Concatenation error:', err)
+ reject(err)
+ })
+ .mergeToFile(fullVideoPath, assetPath.model)
+ })
+}
+
+
+async function submitVideoTask(audioPath, model) {
+ const uuid = crypto.randomUUID()
+ const relativeAudioPath = path.relative(assetPath.ttsRoot, audioPath)
+
+ const param = {
+ audio_url: relativeAudioPath.replace(/\\/g, '/'),
+ video_url: model.video_path,
+ code: uuid,
+ chaofen: 0,
+ watermark_switch: 0,
+ pn: 1
+ }
+
+ const f2fAudioDir = path.dirname(path.join(assetPath.ttsRoot, relativeAudioPath))
+ if (!fs.existsSync(f2fAudioDir)) {
+ fs.mkdirSync(f2fAudioDir, { recursive: true })
+ }
+ fs.copyFileSync(audioPath, path.join(assetPath.ttsRoot, relativeAudioPath))
+
+ const result = await makeVideoApi(param)
+ log.info('Real-time service: video chunk synthesis result:', result)
+ return result
+}
+
+
+async function pollForVideo(taskCode) {
+ let attempts = 0
+ while (attempts < 60) { // Poll for 30 seconds
+ const statusRes = await getVideoStatus(taskCode)
+ if (statusRes.code === 10000 && statusRes.data.status === 2) {
+ return statusRes.data.result
+ }
+ await new Promise(resolve => setTimeout(resolve, 500))
+ attempts++
+ }
+ return null
+}
+
+function startTiktokStream(serverUrl, streamKey, ws, videoPath) {
+ const rtmpUrl = `${serverUrl}/${streamKey}`
+
+ ffmpeg(videoPath)
+ .inputOptions('-re')
+ .videoCodec('libx264')
+ .audioCodec('aac')
+ .toFormat('flv')
+ .save(rtmpUrl)
+ .on('start', (commandLine) => {
+ log.info('ffmpeg started with command:', commandLine)
+ ws.send(JSON.stringify({ type: 'tiktok-stream-started' }))
+ })
+ .on('error', (err) => {
+ log.error('ffmpeg error:', err)
+ ws.send(JSON.stringify({ type: 'tiktok-stream-error', data: err.message }))
+ })
+ .on('end', () => {
+ log.info('ffmpeg finished streaming')
+ ws.send(JSON.stringify({ type: 'tiktok-stream-finished' }))
+ })
+}
+
+
+export { init }
diff --git a/src/main/service/tiktok.js b/src/main/service/tiktok.js
new file mode 100644
index 00000000..1958048b
--- /dev/null
+++ b/src/main/service/tiktok.js
@@ -0,0 +1,35 @@
+import { WebcastPushConnection } from 'tiktok-live-connector'
+import log from '../logger.js'
+
+let connection
+
+function connectToTiktok(username, commentCallback) {
+ if (connection) {
+ connection.disconnect()
+ }
+
+ connection = new WebcastPushConnection(username)
+
+ connection.on('chat', (data) => {
+ log.info(`TikTok comment from ${data.uniqueId}: ${data.comment}`)
+ commentCallback(data.comment)
+ })
+
+ connection.on('connected', () => {
+ log.info(`Connected to TikTok live stream for user ${username}`)
+ })
+
+ connection.on('disconnected', () => {
+ log.info('Disconnected from TikTok live stream')
+ })
+
+ connection.on('error', (err) => {
+ log.error('TikTok connection error:', err)
+ })
+
+ connection.connect().catch(err => {
+ log.error('Failed to connect to TikTok:', err)
+ })
+}
+
+export { connectToTiktok }
diff --git a/src/main/service/video.js b/src/main/service/video.js
index 19bc2c94..40446178 100644
--- a/src/main/service/video.js
+++ b/src/main/service/video.js
@@ -16,7 +16,7 @@ import {
import { makeAudio4Video, copyAudio4Video } from './voice.js'
import { makeVideo as makeVideoApi,getVideoStatus } from '../api/f2f.js'
import log from '../logger.js'
-import { getVideoDuration } from '../util/ffmpeg.js'
+import { getVideoDuration, createVideoFromImage } from '../util/ffmpeg.js'
const MODEL_NAME = 'video'
@@ -123,7 +123,18 @@ export async function synthesisVideo(videoId) {
// 写死调试
({ result, param } = await makeVideoByF2F('test.wav', 'test.mp4'))
} else {
- ({ result, param } = await makeVideoByF2F(audioPath, model.video_path))
+ let videoPath = model.video_path
+ if (model.is_image) {
+ const imagePath = path.join(assetPath.model, model.cover_path)
+ const audioDuration = await getVideoDuration(path.join(assetPath.ttsProduct, audioPath))
+ const tempVideoPath = path.join(assetPath.model, `temp-${crypto.randomUUID()}.mp4`)
+ await createVideoFromImage(imagePath, audioDuration, tempVideoPath)
+ videoPath = path.relative(assetPath.model, tempVideoPath)
+ }
+ ({ result, param } = await makeVideoByF2F(audioPath, videoPath))
+ if (model.is_image) {
+ fs.unlinkSync(path.join(assetPath.model, videoPath))
+ }
}
log.debug('~ makeVideo ~ result, param:', result, param)
diff --git a/src/main/service/voice.js b/src/main/service/voice.js
index 69478f85..9e946fcb 100644
--- a/src/main/service/voice.js
+++ b/src/main/service/voice.js
@@ -95,4 +95,7 @@ export function init() {
ipcMain.handle(MODEL_NAME + '/audition', (event, ...args) => {
return audition(...args)
})
+ ipcMain.handle(MODEL_NAME + '/getAllTimbre', (event, ...args) => {
+ return getAllTimbre(...args)
+ })
}
\ No newline at end of file
diff --git a/src/main/util/ffmpeg.js b/src/main/util/ffmpeg.js
index ed79d490..b2ddef77 100644
--- a/src/main/util/ffmpeg.js
+++ b/src/main/util/ffmpeg.js
@@ -123,4 +123,23 @@ export function getVideoDuration(videoPath) {
}
})
})
+}
+
+export function createVideoFromImage(imagePath, duration, outputPath) {
+ return new Promise((resolve, reject) => {
+ ffmpeg(imagePath)
+ .loop(duration)
+ .inputOptions('-framerate 25')
+ .videoCodec('libx264')
+ .outputOptions('-pix_fmt yuv420p')
+ .toFormat('mp4')
+ .save(outputPath)
+ .on('end', () => {
+ log.info('video created from image done')
+ resolve(true)
+ })
+ .on('error', (err) => {
+ reject(err)
+ })
+ })
}
\ No newline at end of file
diff --git a/src/renderer/src/api/index.js b/src/renderer/src/api/index.js
index a492688a..f9b96529 100644
--- a/src/renderer/src/api/index.js
+++ b/src/renderer/src/api/index.js
@@ -41,8 +41,8 @@ export function findModel(id) {
return window.electron.ipcRenderer.invoke('model/find', id)
}
-export function addModel({ name, videoPath }) {
- return window.electron.ipcRenderer.invoke('model/addModel', name, videoPath)
+export function addModel({ name, videoPath, isImage }) {
+ return window.electron.ipcRenderer.invoke('model/addModel', name, videoPath, isImage)
}
export function countModel(name = '') {
@@ -64,3 +64,7 @@ export function saveContext(key, val) {
export function audition(voiceId, text) {
return window.electron.ipcRenderer.invoke('voice/audition', voiceId, text)
}
+
+export function getAllTimbre() {
+ return window.electron.ipcRenderer.invoke('voice/getAllTimbre')
+}
diff --git a/src/renderer/src/components/ImageUpload.vue b/src/renderer/src/components/ImageUpload.vue
new file mode 100644
index 00000000..8a6fd771
--- /dev/null
+++ b/src/renderer/src/components/ImageUpload.vue
@@ -0,0 +1,84 @@
+
+
+
+