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 @@ + + + diff --git a/src/renderer/src/components/ImageUploadDialog.vue b/src/renderer/src/components/ImageUploadDialog.vue new file mode 100644 index 00000000..6ca7bce6 --- /dev/null +++ b/src/renderer/src/components/ImageUploadDialog.vue @@ -0,0 +1,120 @@ + + + diff --git a/src/renderer/src/components/ImageUploadDialog/index.js b/src/renderer/src/components/ImageUploadDialog/index.js new file mode 100644 index 00000000..ce19e60c --- /dev/null +++ b/src/renderer/src/components/ImageUploadDialog/index.js @@ -0,0 +1,26 @@ +import { createApp } from 'vue' +import TDesign from 'tdesign-vue-next' +import ImageUploadDialog from '../ImageUploadDialog.vue' +import i18n from "@renderer/i18n/index.js"; + +export const createImageModel = (props = {}) => { + const rootNode = document.createElement('div') + document.body.appendChild(rootNode) + + return new Promise((resolve) => { + const options = { + ...props, + visible: true, + onClose(result) { + resolve(result) + }, + onClosed: () => { + app.unmount() + document.body.removeChild(rootNode) + } + } + + const app = createApp(ImageUploadDialog, options).use(TDesign).use(i18n) + app.mount(rootNode) + }) +} diff --git a/src/renderer/src/components/menuLIst.vue b/src/renderer/src/components/menuLIst.vue index a87314a4..59e93748 100644 --- a/src/renderer/src/components/menuLIst.vue +++ b/src/renderer/src/components/menuLIst.vue @@ -29,6 +29,14 @@ const obj = [ offIcon, active: true, path: '/home' + }, + { + key: 'common.menu.realtime', + name: t('common.menu.realtime'), + onIcon, + offIcon, + active: false, + path: '/realtime' } /* { name: "账号", diff --git a/src/renderer/src/i18n/components/common.js b/src/renderer/src/i18n/components/common.js index 6ca748eb..86ab7637 100644 --- a/src/renderer/src/i18n/components/common.js +++ b/src/renderer/src/i18n/components/common.js @@ -7,7 +7,8 @@ import { Button } from 'tdesign-vue-next' // 中文翻译 export const commonZh = { menu: { - text: '首页' + text: '首页', + realtime: '实时互动' }, header: { minimizeText: '最小化', @@ -148,7 +149,8 @@ export const commonZh = { // 英文翻译 export const commonEn = { menu: { - text: 'Home' + text: 'Home', + realtime: 'Real-time' }, header: { minimizeText: 'Minimize', diff --git a/src/renderer/src/router/index.js b/src/renderer/src/router/index.js index 295177c1..1e912935 100644 --- a/src/renderer/src/router/index.js +++ b/src/renderer/src/router/index.js @@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router' import home from '@renderer/views/home/index.vue' import account from '@renderer/views/account/index.vue' import VideoEditView from '@renderer/views/video-edit/VideoEditView.vue' +import RealtimeView from '@renderer/views/realtime/RealtimeView.vue' const router = createRouter({ history: createWebHashHistory(), @@ -26,6 +27,11 @@ const router = createRouter({ name: 'account', component: account }, + { + path: '/realtime', + name: 'realtime', + component: RealtimeView + }, ] }) diff --git a/src/renderer/src/views/home/components/myModelList.vue b/src/renderer/src/views/home/components/myModelList.vue index a7c442f8..65e5375c 100644 --- a/src/renderer/src/views/home/components/myModelList.vue +++ b/src/renderer/src/views/home/components/myModelList.vue @@ -11,6 +11,7 @@ + {{ $t('common.upload.imageText') }}