diff --git a/asr-server/embedded.js b/asr-server/embedded.js new file mode 100644 index 0000000000..68ae30e239 --- /dev/null +++ b/asr-server/embedded.js @@ -0,0 +1,123 @@ +/** + * 内置的ASR服务器模块 + * 这个文件可以直接在Electron中运行,不需要外部依赖 + */ + +// 使用Electron内置的Node.js模块 +const http = require('http') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server (Embedded) starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建HTTP服务器 +const server = http.createServer((req, res) => { + try { + if (req.url === '/' || req.url === '/index.html') { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + // 读取文件内容并发送 + fs.readFile(indexPath, (err, data) => { + if (err) { + console.error('Error reading index.html:', err) + res.writeHead(500) + res.end('Error reading index.html') + return + } + + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(data) + }) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:34515

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } else { + // 处理其他请求 + res.writeHead(404) + res.end('Not found') + } + } catch (error) { + console.error('Error handling request:', error) + res.writeHead(500) + res.end('Server error') + } +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + const port = 34515 + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/asr-server/index.html b/asr-server/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/asr-server/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/asr-server/package-lock.json b/asr-server/package-lock.json new file mode 100644 index 0000000000..8d3eb4035d --- /dev/null +++ b/asr-server/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cherry-asr-server", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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 + } + } + } + } +} diff --git a/asr-server/package.json b/asr-server/package.json new file mode 100644 index 0000000000..b09528cca8 --- /dev/null +++ b/asr-server/package.json @@ -0,0 +1,10 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } +} \ No newline at end of file diff --git a/asr-server/server.js b/asr-server/server.js new file mode 100644 index 0000000000..ed73ecb022 --- /dev/null +++ b/asr-server/server.js @@ -0,0 +1,269 @@ +// 检查依赖项 +try { + console.log('ASR Server starting...') + console.log('Node.js version:', process.version) + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + console.log('Command line arguments:', process.argv) + + // 检查必要的依赖项 + const checkDependency = (name) => { + try { + require(name) // Removed unused variable 'module' + console.log(`Successfully loaded dependency: ${name}`) + return true + } catch (error) { + console.error(`Failed to load dependency: ${name}`, error.message) + return false + } + } + + // 检查所有必要的依赖项 + const dependencies = ['http', 'ws', 'express', 'path', 'fs'] + const missingDeps = dependencies.filter((dep) => !checkDependency(dep)) + + if (missingDeps.length > 0) { + console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`) + process.exit(1) + } +} catch (error) { + console.error('Error during dependency check:', error) + process.exit(1) +} + +// 加载依赖项 +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module +// const fs = require('fs') // Commented out unused import 'fs' + +const app = express() +const port = 34515 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + const fs = require('fs') + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + + // 尝试多个可能的路径 + const possiblePaths = [ + // 开发环境路径 + path.join(__dirname, 'index.html'), + // 当前目录 + path.join(process.cwd(), 'index.html'), + // 相对于可执行文件的路径 + path.join(path.dirname(process.execPath), 'index.html'), + // 相对于可执行文件的上级目录的路径 + path.join(path.dirname(path.dirname(process.execPath)), 'index.html'), + // 相对于可执行文件的resources目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'index.html'), + // 相对于可执行文件的resources/asr-server目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'), + // 相对于可执行文件的asr-server目录的路径 + path.join(path.dirname(process.execPath), 'asr-server', 'index.html'), + // 如果是pkg打包环境 + process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null + ].filter(Boolean) // 过滤掉null值 + + console.log('Possible index.html paths:', possiblePaths) + + // 检查每个路径,返回第一个存在的文件 + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + console.log(`Found index.html at: ${p}`) + return p + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + // 如果没有找到文件,返回默认路径并记录错误 + console.error('Could not find index.html in any of the expected locations') + return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在 +} + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + + // 检查文件是否存在 + const fs = require('fs') + if (!fs.existsSync(indexPath)) { + console.error(`Error: index.html not found at ${indexPath}`) + return res.status(404).send(`Error: index.html not found at ${indexPath}.
Please check the server logs.`) + } + + res.sendFile(indexPath, (err) => { + if (err) { + console.error('Error sending index.html:', err) + res.status(500).send(`Error serving index.html: ${err.message}`) + } + }) + } catch (error) { + console.error('Error in route handler:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = ws + console.log('[Server] Browser identified and connected') + // Notify Electron that the browser is ready + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent browser_ready status to Electron') + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = ws + console.log('[Server] Electron identified and connected') + // If browser is already connected when Electron connects, notify Electron immediately + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent initial browser_ready status to Electron') + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } else if (data.type === 'reset' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying RESET command to browser') + browserConnection.send(JSON.stringify({ type: 'reset' })) + } else { + console.log('[Server] Cannot relay RESET: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // Handle server errors + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/asr-server/standalone.js b/asr-server/standalone.js new file mode 100644 index 0000000000..7b826d1f25 --- /dev/null +++ b/asr-server/standalone.js @@ -0,0 +1,114 @@ +/** + * 独立的ASR服务器 + * 这个文件是一个简化版的server.js,用于在打包后的应用中运行 + */ + +// 基本依赖 +const http = require('http') +const express = require('express') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建Express应用 +const app = express() +const port = 34515 + +// 提供静态文件 +app.use(express.static(__dirname)) + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + res.sendFile(indexPath) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.send(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:${port}

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } catch (error) { + console.error('Error serving index.html:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +// 创建HTTP服务器 +const server = http.createServer(app) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/asr-server/start-server.bat b/asr-server/start-server.bat new file mode 100644 index 0000000000..3f9628a75c --- /dev/null +++ b/asr-server/start-server.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting ASR Server... +cd /d %~dp0 +node standalone.js +pause diff --git a/electron-builder.yml b/electron-builder.yml index 33784cbfdd..e68b4cd71a 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -9,6 +9,8 @@ electronLanguages: directories: buildResources: build files: + - out/**/* + - package.json - '!{.vscode,.yarn,.github}' - '!electron.vite.config.{js,ts,mjs,cjs}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' @@ -33,9 +35,18 @@ files: - '!node_modules/@tavily/core/node_modules/js-tiktoken' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' -asarUnpack: +asarUnpack: # Removed ASR server rules from 'files' section - resources/** - '**/*.{metal,exp,lib}' +extraResources: # Add extraResources to copy the prepared asr-server directory + - from: asr-server # Copy the folder from project root + to: app/asr-server # Copy TO the 'app' subfolder within resources + filter: + - '**/*' # Include everything inside + - from: resources/data # Copy the data folder with agents.json + to: data # Copy TO the 'data' subfolder within resources + filter: + - '**/*' # Include everything inside win: executableName: Cherry Studio artifactName: ${productName}-${version}-${arch}-setup.${ext} diff --git a/electron.vite.config.ts b/electron.vite.config.ts index d0ceafc025..2d81d04582 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -76,6 +76,17 @@ export default defineConfig({ }, optimizeDeps: { exclude: [] + }, + build: { + rollupOptions: { + input: { + index: resolve('src/renderer/index.html') + } + }, + // 复制ASR服务器文件 + assetsInlineLimit: 0, + // 确保复制assets目录下的所有文件 + copyPublicDir: true } } }) diff --git a/index.html b/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/package.json b/package.json index 5435dbb887..a44ee7dcf1 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "d3": "^7.9.0", "diff": "^7.0.0", "docx": "^9.0.2", + "edge-tts-node": "^1.5.7", "electron-log": "^5.1.5", "electron-store": "^8.2.0", "electron-updater": "^6.3.9", @@ -90,6 +91,7 @@ "js-yaml": "^4.1.0", "jsdom": "^26.0.0", "markdown-it": "^14.1.0", + "node-edge-tts": "^1.2.8", "officeparser": "^4.1.1", "proxy-agent": "^6.5.0", "tar": "^7.4.3", @@ -126,7 +128,7 @@ "@types/diff": "^7", "@types/fs-extra": "^11", "@types/js-yaml": "^4", - "@types/lodash": "^4.17.5", + "@types/lodash": "^4.17.16", "@types/markdown-it": "^14", "@types/md5": "^2.3.5", "@types/node": "^18.19.9", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index eed0bfa1ad..b0e25c106f 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -18,6 +18,17 @@ export enum IpcChannel { App_InstallUvBinary = 'app:install-uv-binary', App_InstallBunBinary = 'app:install-bun-binary', + // ASR Server + Asr_StartServer = 'start-asr-server', + Asr_StopServer = 'stop-asr-server', + + // MsTTS + MsTTS_GetVoices = 'mstts:get-voices', + MsTTS_Synthesize = 'mstts:synthesize', + MsTTS_SynthesizeStream = 'mstts:synthesize-stream', + MsTTS_StreamData = 'mstts:stream-data', + MsTTS_StreamEnd = 'mstts:stream-end', + // Open Open_Path = 'open:path', Open_Website = 'open:website', diff --git a/public/asr-server/embedded.js b/public/asr-server/embedded.js new file mode 100644 index 0000000000..68ae30e239 --- /dev/null +++ b/public/asr-server/embedded.js @@ -0,0 +1,123 @@ +/** + * 内置的ASR服务器模块 + * 这个文件可以直接在Electron中运行,不需要外部依赖 + */ + +// 使用Electron内置的Node.js模块 +const http = require('http') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server (Embedded) starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建HTTP服务器 +const server = http.createServer((req, res) => { + try { + if (req.url === '/' || req.url === '/index.html') { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + // 读取文件内容并发送 + fs.readFile(indexPath, (err, data) => { + if (err) { + console.error('Error reading index.html:', err) + res.writeHead(500) + res.end('Error reading index.html') + return + } + + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(data) + }) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:34515

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } else { + // 处理其他请求 + res.writeHead(404) + res.end('Not found') + } + } catch (error) { + console.error('Error handling request:', error) + res.writeHead(500) + res.end('Server error') + } +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + const port = 34515 + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/public/asr-server/index.html b/public/asr-server/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/public/asr-server/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/public/asr-server/package-lock.json b/public/asr-server/package-lock.json new file mode 100644 index 0000000000..8d3eb4035d --- /dev/null +++ b/public/asr-server/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cherry-asr-server", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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 + } + } + } + } +} diff --git a/public/asr-server/package.json b/public/asr-server/package.json new file mode 100644 index 0000000000..b09528cca8 --- /dev/null +++ b/public/asr-server/package.json @@ -0,0 +1,10 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } +} \ No newline at end of file diff --git a/public/asr-server/server.js b/public/asr-server/server.js new file mode 100644 index 0000000000..ed73ecb022 --- /dev/null +++ b/public/asr-server/server.js @@ -0,0 +1,269 @@ +// 检查依赖项 +try { + console.log('ASR Server starting...') + console.log('Node.js version:', process.version) + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + console.log('Command line arguments:', process.argv) + + // 检查必要的依赖项 + const checkDependency = (name) => { + try { + require(name) // Removed unused variable 'module' + console.log(`Successfully loaded dependency: ${name}`) + return true + } catch (error) { + console.error(`Failed to load dependency: ${name}`, error.message) + return false + } + } + + // 检查所有必要的依赖项 + const dependencies = ['http', 'ws', 'express', 'path', 'fs'] + const missingDeps = dependencies.filter((dep) => !checkDependency(dep)) + + if (missingDeps.length > 0) { + console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`) + process.exit(1) + } +} catch (error) { + console.error('Error during dependency check:', error) + process.exit(1) +} + +// 加载依赖项 +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module +// const fs = require('fs') // Commented out unused import 'fs' + +const app = express() +const port = 34515 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + const fs = require('fs') + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + + // 尝试多个可能的路径 + const possiblePaths = [ + // 开发环境路径 + path.join(__dirname, 'index.html'), + // 当前目录 + path.join(process.cwd(), 'index.html'), + // 相对于可执行文件的路径 + path.join(path.dirname(process.execPath), 'index.html'), + // 相对于可执行文件的上级目录的路径 + path.join(path.dirname(path.dirname(process.execPath)), 'index.html'), + // 相对于可执行文件的resources目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'index.html'), + // 相对于可执行文件的resources/asr-server目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'), + // 相对于可执行文件的asr-server目录的路径 + path.join(path.dirname(process.execPath), 'asr-server', 'index.html'), + // 如果是pkg打包环境 + process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null + ].filter(Boolean) // 过滤掉null值 + + console.log('Possible index.html paths:', possiblePaths) + + // 检查每个路径,返回第一个存在的文件 + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + console.log(`Found index.html at: ${p}`) + return p + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + // 如果没有找到文件,返回默认路径并记录错误 + console.error('Could not find index.html in any of the expected locations') + return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在 +} + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + + // 检查文件是否存在 + const fs = require('fs') + if (!fs.existsSync(indexPath)) { + console.error(`Error: index.html not found at ${indexPath}`) + return res.status(404).send(`Error: index.html not found at ${indexPath}.
Please check the server logs.`) + } + + res.sendFile(indexPath, (err) => { + if (err) { + console.error('Error sending index.html:', err) + res.status(500).send(`Error serving index.html: ${err.message}`) + } + }) + } catch (error) { + console.error('Error in route handler:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = ws + console.log('[Server] Browser identified and connected') + // Notify Electron that the browser is ready + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent browser_ready status to Electron') + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = ws + console.log('[Server] Electron identified and connected') + // If browser is already connected when Electron connects, notify Electron immediately + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent initial browser_ready status to Electron') + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } else if (data.type === 'reset' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying RESET command to browser') + browserConnection.send(JSON.stringify({ type: 'reset' })) + } else { + console.log('[Server] Cannot relay RESET: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // Handle server errors + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/public/asr-server/standalone.js b/public/asr-server/standalone.js new file mode 100644 index 0000000000..7b826d1f25 --- /dev/null +++ b/public/asr-server/standalone.js @@ -0,0 +1,114 @@ +/** + * 独立的ASR服务器 + * 这个文件是一个简化版的server.js,用于在打包后的应用中运行 + */ + +// 基本依赖 +const http = require('http') +const express = require('express') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建Express应用 +const app = express() +const port = 34515 + +// 提供静态文件 +app.use(express.static(__dirname)) + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + res.sendFile(indexPath) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.send(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:${port}

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } catch (error) { + console.error('Error serving index.html:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +// 创建HTTP服务器 +const server = http.createServer(app) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/public/asr-server/start-server.bat b/public/asr-server/start-server.bat new file mode 100644 index 0000000000..3f9628a75c --- /dev/null +++ b/public/asr-server/start-server.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting ASR Server... +cd /d %~dp0 +node standalone.js +pause diff --git a/resources/asr-server/embedded.js b/resources/asr-server/embedded.js new file mode 100644 index 0000000000..68ae30e239 --- /dev/null +++ b/resources/asr-server/embedded.js @@ -0,0 +1,123 @@ +/** + * 内置的ASR服务器模块 + * 这个文件可以直接在Electron中运行,不需要外部依赖 + */ + +// 使用Electron内置的Node.js模块 +const http = require('http') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server (Embedded) starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建HTTP服务器 +const server = http.createServer((req, res) => { + try { + if (req.url === '/' || req.url === '/index.html') { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + // 读取文件内容并发送 + fs.readFile(indexPath, (err, data) => { + if (err) { + console.error('Error reading index.html:', err) + res.writeHead(500) + res.end('Error reading index.html') + return + } + + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(data) + }) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:34515

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } else { + // 处理其他请求 + res.writeHead(404) + res.end('Not found') + } + } catch (error) { + console.error('Error handling request:', error) + res.writeHead(500) + res.end('Server error') + } +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + const port = 34515 + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/resources/asr-server/index.html b/resources/asr-server/index.html new file mode 100644 index 0000000000..cfa0db82fb --- /dev/null +++ b/resources/asr-server/index.html @@ -0,0 +1,425 @@ + + + + + + + Cherry Studio ASR + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/resources/asr-server/package-lock.json b/resources/asr-server/package-lock.json new file mode 100644 index 0000000000..8d3eb4035d --- /dev/null +++ b/resources/asr-server/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cherry-asr-server", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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 + } + } + } + } +} diff --git a/resources/asr-server/package.json b/resources/asr-server/package.json new file mode 100644 index 0000000000..b09528cca8 --- /dev/null +++ b/resources/asr-server/package.json @@ -0,0 +1,10 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + } +} \ No newline at end of file diff --git a/resources/asr-server/server.js b/resources/asr-server/server.js new file mode 100644 index 0000000000..ed73ecb022 --- /dev/null +++ b/resources/asr-server/server.js @@ -0,0 +1,269 @@ +// 检查依赖项 +try { + console.log('ASR Server starting...') + console.log('Node.js version:', process.version) + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + console.log('Command line arguments:', process.argv) + + // 检查必要的依赖项 + const checkDependency = (name) => { + try { + require(name) // Removed unused variable 'module' + console.log(`Successfully loaded dependency: ${name}`) + return true + } catch (error) { + console.error(`Failed to load dependency: ${name}`, error.message) + return false + } + } + + // 检查所有必要的依赖项 + const dependencies = ['http', 'ws', 'express', 'path', 'fs'] + const missingDeps = dependencies.filter((dep) => !checkDependency(dep)) + + if (missingDeps.length > 0) { + console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`) + process.exit(1) + } +} catch (error) { + console.error('Error during dependency check:', error) + process.exit(1) +} + +// 加载依赖项 +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module +// const fs = require('fs') // Commented out unused import 'fs' + +const app = express() +const port = 34515 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + const fs = require('fs') + console.log('Current directory:', __dirname) + console.log('Current working directory:', process.cwd()) + + // 尝试多个可能的路径 + const possiblePaths = [ + // 开发环境路径 + path.join(__dirname, 'index.html'), + // 当前目录 + path.join(process.cwd(), 'index.html'), + // 相对于可执行文件的路径 + path.join(path.dirname(process.execPath), 'index.html'), + // 相对于可执行文件的上级目录的路径 + path.join(path.dirname(path.dirname(process.execPath)), 'index.html'), + // 相对于可执行文件的resources目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'index.html'), + // 相对于可执行文件的resources/asr-server目录的路径 + path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'), + // 相对于可执行文件的asr-server目录的路径 + path.join(path.dirname(process.execPath), 'asr-server', 'index.html'), + // 如果是pkg打包环境 + process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null + ].filter(Boolean) // 过滤掉null值 + + console.log('Possible index.html paths:', possiblePaths) + + // 检查每个路径,返回第一个存在的文件 + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + console.log(`Found index.html at: ${p}`) + return p + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + // 如果没有找到文件,返回默认路径并记录错误 + console.error('Could not find index.html in any of the expected locations') + return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在 +} + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + + // 检查文件是否存在 + const fs = require('fs') + if (!fs.existsSync(indexPath)) { + console.error(`Error: index.html not found at ${indexPath}`) + return res.status(404).send(`Error: index.html not found at ${indexPath}.
Please check the server logs.`) + } + + res.sendFile(indexPath, (err) => { + if (err) { + console.error('Error sending index.html:', err) + res.status(500).send(`Error serving index.html: ${err.message}`) + } + }) + } catch (error) { + console.error('Error in route handler:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = ws + console.log('[Server] Browser identified and connected') + // Notify Electron that the browser is ready + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent browser_ready status to Electron') + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = ws + console.log('[Server] Electron identified and connected') + // If browser is already connected when Electron connects, notify Electron immediately + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent initial browser_ready status to Electron') + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } else if (data.type === 'reset' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying RESET command to browser') + browserConnection.send(JSON.stringify({ type: 'reset' })) + } else { + console.log('[Server] Cannot relay RESET: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // Handle server errors + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/resources/asr-server/standalone.js b/resources/asr-server/standalone.js new file mode 100644 index 0000000000..7b826d1f25 --- /dev/null +++ b/resources/asr-server/standalone.js @@ -0,0 +1,114 @@ +/** + * 独立的ASR服务器 + * 这个文件是一个简化版的server.js,用于在打包后的应用中运行 + */ + +// 基本依赖 +const http = require('http') +const express = require('express') +const path = require('path') +const fs = require('fs') + +// 输出环境信息 +console.log('ASR Server starting...') +console.log('Node.js version:', process.version) +console.log('Current directory:', __dirname) +console.log('Current working directory:', process.cwd()) +console.log('Command line arguments:', process.argv) + +// 创建Express应用 +const app = express() +const port = 34515 + +// 提供静态文件 +app.use(express.static(__dirname)) + +// 提供网页给浏览器 +app.get('/', (req, res) => { + try { + // 尝试多个可能的路径 + const possiblePaths = [ + // 当前目录 + path.join(__dirname, 'index.html'), + // 上级目录 + path.join(__dirname, '..', 'index.html'), + // 应用根目录 + path.join(process.cwd(), 'index.html') + ] + + console.log('Possible index.html paths:', possiblePaths) + + // 查找第一个存在的文件 + let indexPath = null + for (const p of possiblePaths) { + try { + if (fs.existsSync(p)) { + indexPath = p + console.log(`Found index.html at: ${p}`) + break + } + } catch (e) { + console.error(`Error checking existence of ${p}:`, e) + } + } + + if (indexPath) { + res.sendFile(indexPath) + } else { + // 如果找不到文件,返回一个简单的HTML页面 + console.error('Could not find index.html, serving fallback page') + res.send(` + + + + ASR Server + + + +

ASR Server is running

+

This is a fallback page because the index.html file could not be found.

+

Server is running at: http://localhost:${port}

+

Current directory: ${__dirname}

+

Working directory: ${process.cwd()}

+ + + `) + } + } catch (error) { + console.error('Error serving index.html:', error) + res.status(500).send(`Server error: ${error.message}`) + } +}) + +// 创建HTTP服务器 +const server = http.createServer(app) + +// 添加进程错误处理 +process.on('uncaughtException', (error) => { + console.error('[Server] Uncaught exception:', error) + // 不立即退出,给日志输出的时间 + setTimeout(() => process.exit(1), 1000) +}) + +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) +}) + +// 尝试启动服务器 +try { + server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) + }) + + // 处理服务器错误 + server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start + }) +} catch (error) { + console.error('[Server] Critical error starting server:', error) + process.exit(1) +} diff --git a/resources/asr-server/start-server.bat b/resources/asr-server/start-server.bat new file mode 100644 index 0000000000..3f9628a75c --- /dev/null +++ b/resources/asr-server/start-server.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting ASR Server... +cd /d %~dp0 +node standalone.js +pause diff --git a/scripts/check-i18n.js b/scripts/check-i18n.js index 411ce4d558..747d644edf 100644 --- a/scripts/check-i18n.js +++ b/scripts/check-i18n.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true }) var fs = require('fs') var path = require('path') var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales') -var baseLocale = 'zh-CN' +var baseLocale = 'zh-cn' var baseFileName = ''.concat(baseLocale, '.json') var baseFilePath = path.join(translationsDir, baseFileName) /** diff --git a/scripts/check-i18n.ts b/scripts/check-i18n.ts index 915aa31f4f..08b5ddd609 100644 --- a/scripts/check-i18n.ts +++ b/scripts/check-i18n.ts @@ -2,7 +2,7 @@ import * as fs from 'fs' import * as path from 'path' const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales') -const baseLocale = 'zh-CN' +const baseLocale = 'zh-cn' const baseFileName = `${baseLocale}.json` const baseFilePath = path.join(translationsDir, baseFileName) diff --git a/src/main/index.ts b/src/main/index.ts index ddd41fceb0..59acda25d2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -50,6 +50,9 @@ if (!app.requestSingleInstanceLock()) { registerIpc(mainWindow, app) + // 注意: MsTTS IPC处理程序已在ipc.ts中注册 + // 不需要再次调用registerMsTTSIpcHandlers() + replaceDevtoolsFont(mainWindow) if (process.env.NODE_ENV === 'development') { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 7d6968ad51..12494016f1 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -12,6 +12,7 @@ import log from 'electron-log' import { titleBarOverlayDark, titleBarOverlayLight } from './config' import AppUpdater from './services/AppUpdater' +import { asrServerService } from './services/ASRServerService' import BackupManager from './services/BackupManager' import { configManager } from './services/ConfigManager' import CopilotService from './services/CopilotService' @@ -21,7 +22,11 @@ import FileStorage from './services/FileStorage' import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import mcpService from './services/MCPService' +<<<<<<< HEAD import { memoryFileService } from './services/MemoryFileService' +======= +import * as MsTTSService from './services/MsTTSService' +>>>>>>> origin/1600822305-patch-2 import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -147,7 +152,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { }) ) await fileManager.clearTemp() - await fs.writeFileSync(log.transports.file.getFile().path, '') + fs.writeFileSync(log.transports.file.getFile().path, '') return { success: true } } catch (error: any) { log.error('Failed to clear cache:', error) @@ -305,6 +310,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // search window +<<<<<<< HEAD ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => { await searchService.openSearchWindow(uid) }) @@ -331,4 +337,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.LongTermMemory_SaveData, async (_, data, forceOverwrite = false) => { return await memoryFileService.saveLongTermData(data, forceOverwrite) }) +======= + ipcMain.handle(IpcChannel.SearchWindow_Open, (_, uid: string) => searchService.openSearchWindow(uid)) + ipcMain.handle(IpcChannel.SearchWindow_Close, (_, uid: string) => searchService.closeSearchWindow(uid)) + ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, (_, uid: string, url: string) => + searchService.openUrlInSearchWindow(uid, url) + ) + + // 注册ASR服务器IPC处理程序 + asrServerService.registerIpcHandlers() + + // 注册MsTTS IPC处理程序 + ipcMain.handle(IpcChannel.MsTTS_GetVoices, MsTTSService.getVoices) + ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) => + MsTTSService.synthesize(text, voice, outputFormat) + ) +>>>>>>> origin/1600822305-patch-2 } diff --git a/src/main/services/ASRServerService.ts b/src/main/services/ASRServerService.ts new file mode 100644 index 0000000000..52fa7dd4bd --- /dev/null +++ b/src/main/services/ASRServerService.ts @@ -0,0 +1,131 @@ +import { ChildProcess, spawn } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' + +import { IpcChannel } from '@shared/IpcChannel' +import { app, ipcMain } from 'electron' +import log from 'electron-log' + +/** + * ASR服务器服务,用于管理ASR服务器进程 + */ +class ASRServerService { + private asrServerProcess: ChildProcess | null = null + + /** + * 注册IPC处理程序 + */ + public registerIpcHandlers(): void { + // 启动ASR服务器 + ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this)) + + // 停止ASR服务器 + ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this)) + } + + /** + * 启动ASR服务器 + * @returns Promise<{success: boolean, pid?: number, error?: string}> + */ + private async startServer(): Promise<{ success: boolean; pid?: number; error?: string }> { + try { + if (this.asrServerProcess) { + return { success: true, pid: this.asrServerProcess.pid } + } + + // 获取服务器文件路径 + log.info('App path:', app.getAppPath()) + // 在开发环境和生产环境中使用不同的路径 + let serverPath = '' + const isPackaged = app.isPackaged + + if (isPackaged) { + // 生产环境 (打包后) - 使用 extraResources 复制的路径 + // 注意: 'app' 是 extraResources 配置中 'to' 字段的一部分 + serverPath = path.join(process.resourcesPath, 'app', 'asr-server', 'server.js') + log.info('生产环境,ASR 服务器路径:', serverPath) + } else { + // 开发环境 - 指向项目根目录的 asr-server + serverPath = path.join(app.getAppPath(), 'asr-server', 'server.js') + log.info('开发环境,ASR 服务器路径:', serverPath) + } + + // 注意:删除了 isExeFile 检查逻辑, 假设总是用 node 启动 + // Removed unused variable 'isExeFile' + log.info('ASR服务器路径:', serverPath) + + // 检查文件是否存在 + if (!fs.existsSync(serverPath)) { + return { success: false, error: '服务器文件不存在' } + } + + // 启动服务器进程 + // 始终使用 node 启动 server.js + log.info(`尝试使用 node 启动: ${serverPath}`) + this.asrServerProcess = spawn('node', [serverPath], { + stdio: 'pipe', // 'pipe' 用于捕获输出, 如果需要调试可以临时改为 'inherit' + detached: false // false 通常足够 + }) + + // 处理服务器输出 + this.asrServerProcess.stdout?.on('data', (data) => { + log.info(`[ASR Server] ${data.toString()}`) + }) + + this.asrServerProcess.stderr?.on('data', (data) => { + log.error(`[ASR Server Error] ${data.toString()}`) + }) + + // 处理服务器退出 + this.asrServerProcess.on('close', (code) => { + log.info(`[ASR Server] 进程退出,退出码: ${code}`) + this.asrServerProcess = null + }) + + // 等待一段时间确保服务器启动 + await new Promise((resolve) => setTimeout(resolve, 1000)) + + return { success: true, pid: this.asrServerProcess.pid } + } catch (error) { + log.error('启动ASR服务器失败:', error) + return { success: false, error: (error as Error).message } + } + } + + /** + * 停止ASR服务器 + * @param _event IPC事件 + * @param pid 进程ID + * @returns Promise<{success: boolean, error?: string}> + */ + private async stopServer( + _event: Electron.IpcMainInvokeEvent, + pid?: number + ): Promise<{ success: boolean; error?: string }> { + try { + if (!this.asrServerProcess) { + return { success: true } + } + + // 检查PID是否匹配 + if (pid && this.asrServerProcess.pid !== pid) { + log.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${this.asrServerProcess.pid}) 不匹配`) + } + + // 杀死进程 + this.asrServerProcess.kill() + + // 等待一段时间确保进程已经退出 + await new Promise((resolve) => setTimeout(resolve, 500)) + + this.asrServerProcess = null + return { success: true } + } catch (error) { + log.error('停止ASR服务器失败:', error) + return { success: false, error: (error as Error).message } + } + } +} + +// 导出单例实例 +export const asrServerService = new ASRServerService() diff --git a/src/main/services/FileService.ts b/src/main/services/FileService.ts index 39255e15f7..01837fe382 100644 --- a/src/main/services/FileService.ts +++ b/src/main/services/FileService.ts @@ -1,7 +1,12 @@ import fs from 'node:fs' export default class FileService { - public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) { - return fs.readFileSync(path, 'utf8') + public static async readFile(_: Electron.IpcMainInvokeEvent, path: string, encoding?: BufferEncoding) { + // 如果指定了编码,则返回字符串,否则返回二进制数据 + if (encoding) { + return fs.readFileSync(path, encoding) + } else { + return fs.readFileSync(path) + } } } diff --git a/src/main/services/MsEdgeTTSService.ts b/src/main/services/MsEdgeTTSService.ts new file mode 100644 index 0000000000..21346872ec --- /dev/null +++ b/src/main/services/MsEdgeTTSService.ts @@ -0,0 +1,137 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { app } from 'electron' +import log from 'electron-log' +import { EdgeTTS } from 'node-edge-tts' + +/** + * Microsoft Edge TTS服务 + * 使用Microsoft Edge的在线TTS服务,不需要API密钥 + */ +class MsEdgeTTSService { + private static instance: MsEdgeTTSService + private tempDir: string + + private constructor() { + this.tempDir = path.join(app.getPath('temp'), 'cherry-tts') + + // 确保临时目录存在 + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir, { recursive: true }) + } + } + + /** + * 获取单例实例 + */ + public static getInstance(): MsEdgeTTSService { + if (!MsEdgeTTSService.instance) { + MsEdgeTTSService.instance = new MsEdgeTTSService() + } + return MsEdgeTTSService.instance + } + + /** + * 获取可用的语音列表 + * @returns 语音列表 + */ + public async getVoices(): Promise { + try { + // 返回预定义的中文语音列表 + return [ + { name: 'zh-CN-XiaoxiaoNeural', locale: 'zh-CN', gender: 'Female' }, + { name: 'zh-CN-YunxiNeural', locale: 'zh-CN', gender: 'Male' }, + { name: 'zh-CN-YunyangNeural', locale: 'zh-CN', gender: 'Male' }, + { name: 'zh-CN-XiaohanNeural', locale: 'zh-CN', gender: 'Female' }, + { name: 'zh-CN-XiaomoNeural', locale: 'zh-CN', gender: 'Female' }, + { name: 'zh-CN-XiaoxuanNeural', locale: 'zh-CN', gender: 'Female' }, + { name: 'zh-CN-XiaoruiNeural', locale: 'zh-CN', gender: 'Female' }, + { name: 'zh-CN-YunfengNeural', locale: 'zh-CN', gender: 'Male' } + ] + } catch (error) { + log.error('获取Microsoft Edge TTS语音列表失败:', error) + throw error + } + } + + /** + * 合成语音 + * @param text 要合成的文本 + * @param voice 语音 + * @param outputFormat 输出格式 + * @returns 音频文件路径 + */ + public async synthesize(text: string, voice: string, outputFormat: string): Promise { + try { + log.info(`Microsoft Edge TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`) + + // 验证输入参数 + if (!text || text.trim() === '') { + throw new Error('要合成的文本不能为空') + } + + if (!voice || voice.trim() === '') { + throw new Error('语音名称不能为空') + } + + // 创建一个新的EdgeTTS实例,并设置参数 + const tts = new EdgeTTS({ + voice: voice, + outputFormat: outputFormat, + timeout: 30000, // 30秒超时 + rate: '+0%', // 正常语速 + pitch: '+0Hz', // 正常音调 + volume: '+0%' // 正常音量 + }) + + // 生成临时文件路径 + const timestamp = Date.now() + const fileExtension = outputFormat.includes('mp3') ? 'mp3' : outputFormat.split('-').pop() || 'audio' + const outputPath = path.join(this.tempDir, `tts_${timestamp}.${fileExtension}`) + + log.info(`开始生成语音文件: ${outputPath}`) + + // 使用ttsPromise方法生成文件 + await tts.ttsPromise(text, outputPath) + + // 验证生成的文件是否存在且大小大于0 + if (!fs.existsSync(outputPath)) { + throw new Error(`生成的语音文件不存在: ${outputPath}`) + } + + const stats = fs.statSync(outputPath) + if (stats.size === 0) { + throw new Error(`生成的语音文件大小为0: ${outputPath}`) + } + + log.info(`Microsoft Edge TTS合成成功: ${outputPath}, 文件大小: ${stats.size} 字节`) + return outputPath + } catch (error: any) { + // 记录详细的错误信息 + log.error(`Microsoft Edge TTS语音合成失败 (语音=${voice}):`, error) + + // 尝试提供更有用的错误信息 + if (error.message && typeof error.message === 'string') { + if (error.message.includes('Timed out')) { + throw new Error(`语音合成超时,请检查网络连接或尝试其他语音`) + } else if (error.message.includes('ENOTFOUND')) { + throw new Error(`无法连接到Microsoft语音服务,请检查网络连接`) + } else if (error.message.includes('ECONNREFUSED')) { + throw new Error(`连接被拒绝,请检查网络设置或代理配置`) + } + } + + throw error + } + } +} + +// 导出单例方法 +export const getVoices = async () => { + return await MsEdgeTTSService.getInstance().getVoices() +} + +export const synthesize = async (text: string, voice: string, outputFormat: string) => { + return await MsEdgeTTSService.getInstance().synthesize(text, voice, outputFormat) +} diff --git a/src/main/services/MsTTSIpcHandler.ts b/src/main/services/MsTTSIpcHandler.ts new file mode 100644 index 0000000000..95096c506a --- /dev/null +++ b/src/main/services/MsTTSIpcHandler.ts @@ -0,0 +1,50 @@ +import { IpcChannel } from '@shared/IpcChannel' +import { BrowserWindow, ipcMain } from 'electron' + +import * as MsTTSService from './MsTTSService' + +/** + * 注册MsTTS相关的IPC处理程序 + */ +export function registerMsTTSIpcHandlers(): void { + // 获取可用的语音列表 + ipcMain.handle(IpcChannel.MsTTS_GetVoices, MsTTSService.getVoices) + + // 合成语音 + ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) => + MsTTSService.synthesize(text, voice, outputFormat) + ) + + // 流式合成语音 + ipcMain.handle( + IpcChannel.MsTTS_SynthesizeStream, + async (event, requestId: string, text: string, voice: string, outputFormat: string) => { + const window = BrowserWindow.fromWebContents(event.sender) + if (!window) return + + try { + await MsTTSService.synthesizeStream( + text, + voice, + outputFormat, + (chunk: Uint8Array) => { + // 发送音频数据块 + if (!window.isDestroyed()) { + window.webContents.send(IpcChannel.MsTTS_StreamData, requestId, chunk) + } + }, + () => { + // 发送流结束信号 + if (!window.isDestroyed()) { + window.webContents.send(IpcChannel.MsTTS_StreamEnd, requestId) + } + } + ) + return { success: true } + } catch (error) { + console.error('流式TTS合成失败:', error) + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + } + ) +} diff --git a/src/main/services/MsTTSService.ts b/src/main/services/MsTTSService.ts new file mode 100644 index 0000000000..b2112c8685 --- /dev/null +++ b/src/main/services/MsTTSService.ts @@ -0,0 +1,643 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node' // 新版支持流式的TTS库 +import { app } from 'electron' +import log from 'electron-log' +import { EdgeTTS } from 'node-edge-tts' // 旧版TTS库 + +// --- START OF HARDCODED VOICE LIST --- +// WARNING: This list is static and may become outdated. +// It's generally recommended to use listVoices() for the most up-to-date list. +const hardcodedVoices = [ + { + Name: 'Microsoft Server Speech Text to Speech Voice (af-ZA, AdriNeural)', + ShortName: 'af-ZA-AdriNeural', + Gender: 'Female', + Locale: 'af-ZA' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (am-ET, MekdesNeural)', + ShortName: 'am-ET-MekdesNeural', + Gender: 'Female', + Locale: 'am-ET' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (ar-AE, FatimaNeural)', + ShortName: 'ar-AE-FatimaNeural', + Gender: 'Female', + Locale: 'ar-AE' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (ar-AE, HamdanNeural)', + ShortName: 'ar-AE-HamdanNeural', + Gender: 'Male', + Locale: 'ar-AE' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (ar-BH, AliNeural)', + ShortName: 'ar-BH-AliNeural', + Gender: 'Male', + Locale: 'ar-BH' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (ar-BH, LailaNeural)', + ShortName: 'ar-BH-LailaNeural', + Gender: 'Female', + Locale: 'ar-BH' + }, + // ... (Many other Arabic locales/voices) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (ar-SA, ZariyahNeural)', + ShortName: 'ar-SA-ZariyahNeural', + Gender: 'Female', + Locale: 'ar-SA' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (az-AZ, BabekNeural)', + ShortName: 'az-AZ-BabekNeural', + Gender: 'Male', + Locale: 'az-AZ' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (az-AZ, BanuNeural)', + ShortName: 'az-AZ-BanuNeural', + Gender: 'Female', + Locale: 'az-AZ' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (bg-BG, BorislavNeural)', + ShortName: 'bg-BG-BorislavNeural', + Gender: 'Male', + Locale: 'bg-BG' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (bg-BG, KalinaNeural)', + ShortName: 'bg-BG-KalinaNeural', + Gender: 'Female', + Locale: 'bg-BG' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (bn-BD, NabanitaNeural)', + ShortName: 'bn-BD-NabanitaNeural', + Gender: 'Female', + Locale: 'bn-BD' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (bn-BD, PradeepNeural)', + ShortName: 'bn-BD-PradeepNeural', + Gender: 'Male', + Locale: 'bn-BD' + }, + // ... (Catalan, Czech, Welsh, Danish, German, Greek, English variants) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-AU, NatashaNeural)', + ShortName: 'en-AU-NatashaNeural', + Gender: 'Female', + Locale: 'en-AU' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-AU, WilliamNeural)', + ShortName: 'en-AU-WilliamNeural', + Gender: 'Male', + Locale: 'en-AU' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-CA, ClaraNeural)', + ShortName: 'en-CA-ClaraNeural', + Gender: 'Female', + Locale: 'en-CA' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-CA, LiamNeural)', + ShortName: 'en-CA-LiamNeural', + Gender: 'Male', + Locale: 'en-CA' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, LibbyNeural)', + ShortName: 'en-GB-LibbyNeural', + Gender: 'Female', + Locale: 'en-GB' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, MaisieNeural)', + ShortName: 'en-GB-MaisieNeural', + Gender: 'Female', + Locale: 'en-GB' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, RyanNeural)', + ShortName: 'en-GB-RyanNeural', + Gender: 'Male', + Locale: 'en-GB' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, SoniaNeural)', + ShortName: 'en-GB-SoniaNeural', + Gender: 'Female', + Locale: 'en-GB' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, ThomasNeural)', + ShortName: 'en-GB-ThomasNeural', + Gender: 'Male', + Locale: 'en-GB' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-HK, SamNeural)', + ShortName: 'en-HK-SamNeural', + Gender: 'Male', + Locale: 'en-HK' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-HK, YanNeural)', + ShortName: 'en-HK-YanNeural', + Gender: 'Female', + Locale: 'en-HK' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-IE, ConnorNeural)', + ShortName: 'en-IE-ConnorNeural', + Gender: 'Male', + Locale: 'en-IE' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-IE, EmilyNeural)', + ShortName: 'en-IE-EmilyNeural', + Gender: 'Female', + Locale: 'en-IE' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-IN, NeerjaNeural)', + ShortName: 'en-IN-NeerjaNeural', + Gender: 'Female', + Locale: 'en-IN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-IN, PrabhatNeural)', + ShortName: 'en-IN-PrabhatNeural', + Gender: 'Male', + Locale: 'en-IN' + }, + // ... (Many more English variants: KE, NG, NZ, PH, SG, TZ, US, ZA) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)', + ShortName: 'en-US-AriaNeural', + Gender: 'Female', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, AnaNeural)', + ShortName: 'en-US-AnaNeural', + Gender: 'Female', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, ChristopherNeural)', + ShortName: 'en-US-ChristopherNeural', + Gender: 'Male', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, EricNeural)', + ShortName: 'en-US-EricNeural', + Gender: 'Male', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, GuyNeural)', + ShortName: 'en-US-GuyNeural', + Gender: 'Male', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, JennyNeural)', + ShortName: 'en-US-JennyNeural', + Gender: 'Female', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, MichelleNeural)', + ShortName: 'en-US-MichelleNeural', + Gender: 'Female', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, RogerNeural)', + ShortName: 'en-US-RogerNeural', + Gender: 'Male', + Locale: 'en-US' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (en-US, SteffanNeural)', + ShortName: 'en-US-SteffanNeural', + Gender: 'Male', + Locale: 'en-US' + }, + // ... (Spanish variants) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (es-MX, DaliaNeural)', + ShortName: 'es-MX-DaliaNeural', + Gender: 'Female', + Locale: 'es-MX' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (es-MX, JorgeNeural)', + ShortName: 'es-MX-JorgeNeural', + Gender: 'Male', + Locale: 'es-MX' + }, + // ... (Estonian, Basque, Persian, Finnish, Filipino, French, Irish, Galician, Gujarati, Hebrew, Hindi, Croatian, Hungarian, Indonesian, Icelandic, Italian, Japanese) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)', + ShortName: 'ja-JP-KeitaNeural', + Gender: 'Male', + Locale: 'ja-JP' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)', + ShortName: 'ja-JP-NanamiNeural', + Gender: 'Female', + Locale: 'ja-JP' + }, + // ... (Javanese, Georgian, Kazakh, Khmer, Kannada, Korean) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (ko-KR, InJoonNeural)', + ShortName: 'ko-KR-InJoonNeural', + Gender: 'Male', + Locale: 'ko-KR' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (ko-KR, SunHiNeural)', + ShortName: 'ko-KR-SunHiNeural', + Gender: 'Female', + Locale: 'ko-KR' + }, + // ... (Lao, Lithuanian, Latvian, Macedonian, Malayalam, Mongolian, Marathi, Malay, Maltese, Burmese, Norwegian, Dutch, Polish, Pashto, Portuguese) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (pt-BR, AntonioNeural)', + ShortName: 'pt-BR-AntonioNeural', + Gender: 'Male', + Locale: 'pt-BR' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (pt-BR, FranciscaNeural)', + ShortName: 'pt-BR-FranciscaNeural', + Gender: 'Female', + Locale: 'pt-BR' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (pt-PT, DuarteNeural)', + ShortName: 'pt-PT-DuarteNeural', + Gender: 'Male', + Locale: 'pt-PT' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (pt-PT, RaquelNeural)', + ShortName: 'pt-PT-RaquelNeural', + Gender: 'Female', + Locale: 'pt-PT' + }, + // ... (Romanian, Russian, Sinhala, Slovak, Slovenian, Somali, Albanian, Serbian, Sundanese, Swedish, Swahili, Tamil, Telugu, Thai) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (th-TH, NiwatNeural)', + ShortName: 'th-TH-NiwatNeural', + Gender: 'Male', + Locale: 'th-TH' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (th-TH, PremwadeeNeural)', + ShortName: 'th-TH-PremwadeeNeural', + Gender: 'Female', + Locale: 'th-TH' + }, + // ... (Turkish, Ukrainian, Urdu, Uzbek, Vietnamese) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (vi-VN, HoaiMyNeural)', + ShortName: 'vi-VN-HoaiMyNeural', + Gender: 'Female', + Locale: 'vi-VN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (vi-VN, NamMinhNeural)', + ShortName: 'vi-VN-NamMinhNeural', + Gender: 'Male', + Locale: 'vi-VN' + }, + // ... (Chinese variants) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)', + ShortName: 'zh-CN-XiaoxiaoNeural', + Gender: 'Female', + Locale: 'zh-CN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiNeural)', + ShortName: 'zh-CN-YunxiNeural', + Gender: 'Male', + Locale: 'zh-CN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunjianNeural)', + ShortName: 'zh-CN-YunjianNeural', + Gender: 'Male', + Locale: 'zh-CN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiaNeural)', + ShortName: 'zh-CN-YunxiaNeural', + Gender: 'Male', + Locale: 'zh-CN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunyangNeural)', + ShortName: 'zh-CN-YunyangNeural', + Gender: 'Male', + Locale: 'zh-CN' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN-liaoning, XiaobeiNeural)', + ShortName: 'zh-CN-liaoning-XiaobeiNeural', + Gender: 'Female', + Locale: 'zh-CN-liaoning' + }, + // { Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN-shaanxi, XiaoniNeural)', ShortName: 'zh-CN-shaanxi-XiaoniNeural', Gender: 'Female', Locale: 'zh-CN-shaanxi' }, // Example regional voice + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, HiuGaaiNeural)', + ShortName: 'zh-HK-HiuGaaiNeural', + Gender: 'Female', + Locale: 'zh-HK' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, HiuMaanNeural)', + ShortName: 'zh-HK-HiuMaanNeural', + Gender: 'Female', + Locale: 'zh-HK' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, WanLungNeural)', + ShortName: 'zh-HK-WanLungNeural', + Gender: 'Male', + Locale: 'zh-HK' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoChenNeural)', + ShortName: 'zh-TW-HsiaoChenNeural', + Gender: 'Female', + Locale: 'zh-TW' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoYuNeural)', + ShortName: 'zh-TW-HsiaoYuNeural', + Gender: 'Female', + Locale: 'zh-TW' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, YunJheNeural)', + ShortName: 'zh-TW-YunJheNeural', + Gender: 'Male', + Locale: 'zh-TW' + }, + // ... (Zulu) ... + { + Name: 'Microsoft Server Speech Text to Speech Voice (zu-ZA, ThandoNeural)', + ShortName: 'zu-ZA-ThandoNeural', + Gender: 'Female', + Locale: 'zu-ZA' + }, + { + Name: 'Microsoft Server Speech Text to Speech Voice (zu-ZA, ThembaNeural)', + ShortName: 'zu-ZA-ThembaNeural', + Gender: 'Male', + Locale: 'zu-ZA' + } +] +// --- END OF HARDCODED VOICE LIST --- + +/** + * 免费在线TTS服务 + * 使用免费的在线TTS服务,不需要API密钥 + */ +class MsTTSService { + private static instance: MsTTSService + private tempDir: string + + private constructor() { + this.tempDir = path.join(app.getPath('temp'), 'cherry-tts') + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir, { recursive: true }) + } + log.info('初始化免费在线TTS服务 (使用硬编码语音列表)') + } + + public static getInstance(): MsTTSService { + if (!MsTTSService.instance) { + MsTTSService.instance = new MsTTSService() + } + return MsTTSService.instance + } + + /** + * 流式合成语音 + * @param text 要合成的文本 + * @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural') + * @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3') + * @param onData 数据块回调 + * @param onEnd 结束回调 + */ + public async synthesizeStream( + text: string, + voice: string, + outputFormat: string, + onData: (chunk: Uint8Array) => void, + onEnd: () => void + ): Promise { + try { + // 记录详细的请求信息 + log.info(`流式微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`) + + // 验证输入参数 + if (!text || text.trim() === '') { + throw new Error('要合成的文本不能为空') + } + + if (!voice || voice.trim() === '') { + throw new Error('语音名称不能为空') + } + + // 创建一个新的MsEdgeTTS实例 + const tts = new MsEdgeTTS({ + enableLogger: false // 禁用内部日志 + }) + + // 设置元数据 + let msOutputFormat: OUTPUT_FORMAT + if (outputFormat.includes('mp3')) { + msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3 + } else if (outputFormat.includes('webm')) { + msOutputFormat = OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS + } else { + msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3 + } + + await tts.setMetadata(voice, msOutputFormat) + + // 创建流 + const audioStream = tts.toStream(text) + + // 监听数据事件 + audioStream.on('data', (data: Buffer) => { + onData(data) + }) + + // 监听结束事件 + audioStream.on('end', () => { + log.info(`流式微软在线TTS合成成功`) + onEnd() + }) + + // 监听错误事件 + audioStream.on('error', (error: Error) => { + log.error(`流式微软在线TTS语音合成失败:`, error) + throw error + }) + } catch (error: any) { + // 记录详细的错误信息 + log.error(`流式微软在线TTS语音合成失败 (语音=${voice}):`, error) + throw error + } + } + + /** + * 获取可用的语音列表 (返回硬编码列表) + * @returns 语音列表 + */ + public async getVoices(): Promise { + try { + log.info(`返回硬编码的 ${hardcodedVoices.length} 个语音列表`) + // 直接返回硬编码的列表 + // 注意:保持 async 是为了接口兼容性,虽然这里没有实际的异步操作 + return hardcodedVoices + } catch (error) { + // 这个 try/catch 在这里意义不大了,因为返回静态数据不会出错 + // 但保留结构以防未来改动 + log.error('获取硬编码语音列表时出错 (理论上不应发生):', error) + return [] // 返回空列表以防万一 + } + } + + /** + * 合成语音 + * @param text 要合成的文本 + * @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural') + * @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3') + * @returns 音频文件路径 + */ + public async synthesize(text: string, voice: string, outputFormat: string): Promise { + try { + // 记录详细的请求信息 + log.info(`微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`) + + // 验证输入参数 + if (!text || text.trim() === '') { + throw new Error('要合成的文本不能为空') + } + + if (!voice || voice.trim() === '') { + throw new Error('语音名称不能为空') + } + + // 创建一个新的EdgeTTS实例,并设置参数 + // 添加超时设置,默认为30秒 + const tts = new EdgeTTS({ + voice: voice, + outputFormat: outputFormat, + timeout: 30000, // 30秒超时 + rate: '+0%', // 正常语速 + pitch: '+0Hz', // 正常音调 + volume: '+0%' // 正常音量 + }) + + // 生成临时文件路径 + const timestamp = Date.now() + const fileExtension = outputFormat.includes('mp3') ? 'mp3' : outputFormat.split('-').pop() || 'audio' + const outputPath = path.join(this.tempDir, `tts_${timestamp}.${fileExtension}`) + + log.info(`开始生成语音文件: ${outputPath}`) + + // 使用ttsPromise方法生成文件 + await tts.ttsPromise(text, outputPath) + + // 验证生成的文件是否存在且大小大于0 + if (!fs.existsSync(outputPath)) { + throw new Error(`生成的语音文件不存在: ${outputPath}`) + } + + const stats = fs.statSync(outputPath) + if (stats.size === 0) { + throw new Error(`生成的语音文件大小为0: ${outputPath}`) + } + + log.info(`微软在线TTS合成成功: ${outputPath}, 文件大小: ${stats.size} 字节`) + return outputPath + } catch (error: any) { + // 记录详细的错误信息 + log.error(`微软在线TTS语音合成失败 (语音=${voice}):`, error) + + // 尝试提供更有用的错误信息 + if (error.message && typeof error.message === 'string') { + if (error.message.includes('Timed out')) { + throw new Error(`语音合成超时,请检查网络连接或尝试其他语音`) + } else if (error.message.includes('ENOTFOUND')) { + throw new Error(`无法连接到微软语音服务,请检查网络连接`) + } else if (error.message.includes('ECONNREFUSED')) { + throw new Error(`连接被拒绝,请检查网络设置或代理配置`) + } + } + + throw error + } + } + + /** + * (可选) 清理临时文件目录 + */ + public async cleanupTempDir(): Promise { + // (Cleanup method remains the same) + try { + const files = await fs.promises.readdir(this.tempDir) + for (const file of files) { + if (file.startsWith('tts_')) { + await fs.promises.unlink(path.join(this.tempDir, file)) + } + } + log.info('TTS 临时文件已清理') + } catch (error) { + log.error('清理 TTS 临时文件失败:', error) + } + } +} + +// 导出单例方法 (保持不变) +export const getVoices = async () => { + return await MsTTSService.getInstance().getVoices() +} + +export const synthesize = async (text: string, voice: string, outputFormat: string) => { + return await MsTTSService.getInstance().synthesize(text, voice, outputFormat) +} + +export const synthesizeStream = async ( + text: string, + voice: string, + outputFormat: string, + onData: (chunk: Uint8Array) => void, + onEnd: () => void +) => { + return await MsTTSService.getInstance().synthesizeStream(text, voice, outputFormat, onData, onEnd) +} + +export const cleanupTtsTempFiles = async () => { + await MsTTSService.getInstance().cleanupTempDir() +} diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index 4a6fde670d..9e5b7b8c5d 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -4,7 +4,8 @@ import path from 'node:path' import { app } from 'electron' export function getResourcePath() { - return path.join(app.getAppPath(), 'resources') + // 在打包环境中,使用process.resourcesPath,否则使用app.getAppPath()/resources + return app.isPackaged ? process.resourcesPath : path.join(app.getAppPath(), 'resources') } export function getDataPath() { diff --git a/src/preload/index.ts b/src/preload/index.ts index e12b3d55c4..f771f6de14 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -66,7 +66,7 @@ const api = { binaryFile: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryFile, fileId) }, fs: { - read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path) + read: (path: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, path, encoding) }, export: { toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName) @@ -121,6 +121,11 @@ const api = { toggle: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Toggle), setPin: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.MiniWindow_SetPin, isPinned) }, + msTTS: { + getVoices: () => ipcRenderer.invoke(IpcChannel.MsTTS_GetVoices), + synthesize: (text: string, voice: string, outputFormat: string) => + ipcRenderer.invoke(IpcChannel.MsTTS_Synthesize, text, voice, outputFormat) + }, aes: { encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke(IpcChannel.Aes_Encrypt, text, secretKey, iv), @@ -183,6 +188,7 @@ const api = { closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid), openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url) }, +<<<<<<< HEAD memory: { loadData: () => ipcRenderer.invoke(IpcChannel.Memory_LoadData), saveData: (data: any) => ipcRenderer.invoke(IpcChannel.Memory_SaveData, data), @@ -190,6 +196,11 @@ const api = { loadLongTermData: () => ipcRenderer.invoke(IpcChannel.LongTermMemory_LoadData), saveLongTermData: (data: any, forceOverwrite: boolean = false) => ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite) +======= + asrServer: { + startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer), + stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid) +>>>>>>> origin/1600822305-patch-2 } } diff --git a/src/renderer/index.html b/src/renderer/index.html index 19a44594df..973a6656c8 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -1,42 +1,43 @@ - - - - - Cherry Studio - - + #spinner { + position: fixed; + width: 100vw; + height: 100vh; + flex-direction: row; + justify-content: center; + align-items: center; + display: none; + } - -
-
- -
- - - - + #spinner img { + width: 100px; + border-radius: 50px; + } + + + + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/src/renderer/src/assets/asr-server/index.html b/src/renderer/src/assets/asr-server/index.html new file mode 100644 index 0000000000..c049cb2b21 --- /dev/null +++ b/src/renderer/src/assets/asr-server/index.html @@ -0,0 +1,395 @@ + + + + + + + Browser ASR (External) + + + + +

浏览器语音识别中继页面

+

这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。

+
正在连接到服务器...
+
+ + + + + \ No newline at end of file diff --git a/src/renderer/src/assets/asr-server/package.json b/src/renderer/src/assets/asr-server/package.json new file mode 100644 index 0000000000..75eaaadcb4 --- /dev/null +++ b/src/renderer/src/assets/asr-server/package.json @@ -0,0 +1,27 @@ +{ + "name": "cherry-asr-server", + "version": "1.0.0", + "description": "Cherry Studio ASR Server", + "main": "server.js", + "bin": "server.js", + "scripts": { + "start": "node server.js", + "build": "pkg ." + }, + "pkg": { + "targets": [ + "node16-win-x64" + ], + "outputPath": "dist", + "assets": [ + "index.html" + ] + }, + "dependencies": { + "express": "^4.18.2", + "ws": "^8.13.0" + }, + "devDependencies": { + "pkg": "^5.8.1" + } +} diff --git a/src/renderer/src/assets/asr-server/server.js b/src/renderer/src/assets/asr-server/server.js new file mode 100644 index 0000000000..0840ec619d --- /dev/null +++ b/src/renderer/src/assets/asr-server/server.js @@ -0,0 +1,179 @@ +const http = require('http') +const WebSocket = require('ws') +const express = require('express') +const path = require('path') // Need path module + +const app = express() +const port = 34515 // Define the port + +// 获取index.html文件的路径 +function getIndexHtmlPath() { + // 在开发环境中,直接使用相对路径 + const devPath = path.join(__dirname, 'index.html') + + // 在pkg打包后,文件会被包含在可执行文件中 + // 使用process.pkg检测是否是打包环境 + if (process.pkg) { + // 在打包环境中,使用绝对路径 + return path.join(path.dirname(process.execPath), 'index.html') + } + + // 如果文件存在,返回开发路径 + try { + if (require('fs').existsSync(devPath)) { + return devPath + } + } catch (e) { + console.error('Error checking file existence:', e) + } + + // 如果都不存在,尝试使用当前目录 + return path.join(process.cwd(), 'index.html') +} + +// 提供网页给浏览器 +app.get('/', (req, res) => { + const indexPath = getIndexHtmlPath() + console.log(`Serving index.html from: ${indexPath}`) + res.sendFile(indexPath) +}) + +const server = http.createServer(app) +const wss = new WebSocket.Server({ server }) + +let browserConnection = null +let electronConnection = null + +wss.on('connection', (ws) => { + console.log('[Server] WebSocket client connected') // Add log + + ws.on('message', (message) => { + let data + try { + // Ensure message is treated as string before parsing + data = JSON.parse(message.toString()) + console.log('[Server] Received message:', data) // Log parsed data + } catch (e) { + console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e) + return // Ignore non-JSON messages + } + + // 识别客户端类型 + if (data.type === 'identify') { + if (data.role === 'browser') { + browserConnection = ws + console.log('[Server] Browser identified and connected') + // Notify Electron that the browser is ready + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent browser_ready status to Electron') + } + // Notify Electron if it's already connected + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' })) + } + ws.on('close', () => { + console.log('[Server] Browser disconnected') + browserConnection = null + // Notify Electron + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' })) + } + }) + ws.on('error', (error) => { + console.error('[Server] Browser WebSocket error:', error) + browserConnection = null // Assume disconnected on error + if (electronConnection) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } + }) + } else if (data.role === 'electron') { + electronConnection = ws + console.log('[Server] Electron identified and connected') + // If browser is already connected when Electron connects, notify Electron immediately + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' })) + console.log('[Server] Sent initial browser_ready status to Electron') + } + ws.on('close', () => { + console.log('[Server] Electron disconnected') + electronConnection = null + // Maybe send stop to browser if electron disconnects? + // if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' })); + }) + ws.on('error', (error) => { + console.error('[Server] Electron WebSocket error:', error) + electronConnection = null // Assume disconnected on error + }) + } + } + // Electron 控制开始/停止 + else if (data.type === 'start' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying START command to browser') + browserConnection.send(JSON.stringify({ type: 'start' })) + } else { + console.log('[Server] Cannot relay START: Browser not connected') + // Optionally notify Electron back + electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' })) + } + } else if (data.type === 'stop' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STOP command to browser') + browserConnection.send(JSON.stringify({ type: 'stop' })) + } else { + console.log('[Server] Cannot relay STOP: Browser not connected') + } + } else if (data.type === 'reset' && ws === electronConnection) { + if (browserConnection && browserConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying RESET command to browser') + browserConnection.send(JSON.stringify({ type: 'reset' })) + } else { + console.log('[Server] Cannot relay RESET: Browser not connected') + } + } + // 浏览器发送识别结果 + else if (data.type === 'result' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + // console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed + electronConnection.send(JSON.stringify({ type: 'result', data: data.data })) + } else { + // console.log('[Server] Cannot relay RESULT: Electron not connected'); + } + } + // 浏览器发送状态更新 (例如 'stopped') + else if (data.type === 'status' && ws === browserConnection) { + if (electronConnection && electronConnection.readyState === WebSocket.OPEN) { + console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed + electronConnection.send(JSON.stringify({ type: 'status', message: data.message })) + } else { + console.log('[Server] Cannot relay STATUS: Electron not connected') + } + } else { + console.log('[Server] Received unknown message type or from unknown source:', data) + } + }) + + ws.on('error', (error) => { + // Generic error handling for connection before identification + console.error('[Server] Initial WebSocket connection error:', error) + // Attempt to clean up based on which connection it might be (if identified) + if (ws === browserConnection) { + browserConnection = null + if (electronConnection) + electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' })) + } else if (ws === electronConnection) { + electronConnection = null + } + }) +}) + +server.listen(port, () => { + console.log(`[Server] Server running at http://localhost:${port}`) +}) + +// Handle server errors +server.on('error', (error) => { + console.error(`[Server] Failed to start server:`, error) + process.exit(1) // Exit if server fails to start +}) diff --git a/src/renderer/src/components/ASRButton.tsx b/src/renderer/src/components/ASRButton.tsx new file mode 100644 index 0000000000..11bbaa2bcc --- /dev/null +++ b/src/renderer/src/components/ASRButton.tsx @@ -0,0 +1,243 @@ +import { AudioOutlined, LoadingOutlined } from '@ant-design/icons' +import { useSettings } from '@renderer/hooks/useSettings' +import ASRService from '@renderer/services/ASRService' +import { Button, Tooltip } from 'antd' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface Props { + onTranscribed: (text: string, isFinal?: boolean) => void + disabled?: boolean + style?: React.CSSProperties +} + +const ASRButton: FC = ({ onTranscribed, disabled = false, style }) => { + const { t } = useTranslation() + const { asrEnabled } = useSettings() + const [isRecording, setIsRecording] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [countdown, setCountdown] = useState(0) + const [isCountingDown, setIsCountingDown] = useState(false) + + const handleASR = useCallback(async () => { + if (!asrEnabled) { + window.message.error({ content: t('settings.asr.error.not_enabled'), key: 'asr-error' }) + return + } + + if (isRecording) { + // 停止录音并处理 + setIsRecording(false) + setIsProcessing(true) + try { + // 添加事件监听器,监听服务器发送的stopped消息 + const originalCallback = ASRService.resultCallback + const stopCallback = (text: string) => { + // 如果是空字符串,只重置状态,不调用原始回调 + if (text === '') { + setIsProcessing(false) + return + } + + // 否则调用原始回调并重置状态 + if (originalCallback) originalCallback(text) + setIsProcessing(false) + } + + await ASRService.stopRecording(stopCallback) + } catch (error) { + console.error('ASR error:', error) + setIsProcessing(false) + } + } else { + // 开始录音 + // 显示3秒倒计时,同时立即开始录音 + setIsCountingDown(true) + setCountdown(3) + setIsRecording(true) + + // 立即发送开始信号 + try { + await ASRService.startRecording(onTranscribed) + } catch (error) { + console.error('Failed to start recording:', error) + setIsRecording(false) + setIsCountingDown(false) + return + } + + // 倒计时结束后只隐藏倒计时显示 + setTimeout(() => { + setIsCountingDown(false) + }, 3000) // 3秒倒计时 + } + }, [asrEnabled, isRecording, onTranscribed, t]) + + const handleCancel = useCallback(() => { + if (isCountingDown) { + // 如果在倒计时中,取消倒计时和录音 + setIsCountingDown(false) + setCountdown(0) + // 同时取消录音,因为录音已经开始 + ASRService.cancelRecording() + setIsRecording(false) + } else if (isRecording) { + // 如果已经在录音,取消录音 + ASRService.cancelRecording() + setIsRecording(false) + } + }, [isRecording, isCountingDown]) + + // 倒计时效果 + useEffect(() => { + if (isCountingDown && countdown > 0) { + const timer = setTimeout(() => { + setCountdown(countdown - 1) + }, 1000) + return () => clearTimeout(timer) + } + return undefined // 添加返回值以解决TS7030错误 + }, [countdown, isCountingDown]) + + if (!asrEnabled) { + return null + } + + return ( + + + : isCountingDown ? null : } + onClick={handleASR} + onDoubleClick={handleCancel} + disabled={disabled || isProcessing || (isCountingDown && countdown > 0)} + style={style} + className={isCountingDown ? 'counting-down' : ''}> + {isCountingDown && {countdown}} + + {isCountingDown && ( + + {t('settings.asr.preparing')} ({countdown}) + + )} + + + ) +} + +const ButtonWrapper = styled.div` + position: relative; + display: inline-block; +` + +const CountdownIndicator = styled.div` + position: absolute; + top: -25px; + left: 50%; + transform: translateX(-50%); + background-color: var(--color-primary); + color: white; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + white-space: nowrap; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + animation: pulse 1s infinite; + z-index: 10; + + @keyframes pulse { + 0% { + opacity: 0.7; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.7; + } + } + + &:after { + content: ''; + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid var(--color-primary); + } +` + +const CountdownNumber = styled.span` + font-size: 18px; + font-weight: bold; + animation: zoom 1s infinite; + + @keyframes zoom { + 0% { + transform: scale(0.8); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(0.8); + } + } +` + +const StyledButton = styled(Button)` + min-width: 30px; + height: 30px; + font-size: 16px; + border-radius: 50%; + transition: all 0.3s ease; + color: var(--color-icon); + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0; + border: none; /* 移除边框 */ + &.anticon, + &.iconfont { + transition: all 0.3s ease; + color: var(--color-icon); + } + &:hover { + background-color: var(--color-background-soft); + .anticon, + .iconfont { + color: var(--color-text-1); + } + } + &.active { + background-color: var(--color-primary) !important; + .anticon, + .iconfont { + color: var(--color-white-soft); + } + &:hover { + background-color: var(--color-primary); + } + } + &.counting-down { + font-weight: bold; + background-color: var(--color-primary); + color: var(--color-white-soft); + } +` + +export default ASRButton diff --git a/src/renderer/src/components/DraggableVoiceCallWindow.tsx b/src/renderer/src/components/DraggableVoiceCallWindow.tsx new file mode 100644 index 0000000000..49d3b932b9 --- /dev/null +++ b/src/renderer/src/components/DraggableVoiceCallWindow.tsx @@ -0,0 +1,621 @@ +import { + AudioMutedOutlined, + AudioOutlined, + CloseOutlined, + DownOutlined, + DragOutlined, + PauseCircleOutlined, + PlayCircleOutlined, + SettingOutlined, + SoundOutlined, + UpOutlined +} from '@ant-design/icons' +import { Button, Space, Tooltip } from 'antd' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import styled from 'styled-components' + +import { VoiceCallService } from '../services/VoiceCallService' +import { setIsVoiceCallActive, setLastPlayedMessageId, setSkipNextAutoTTS } from '../store/settings' +import VoiceVisualizer from './VoiceVisualizer' + +interface Props { + visible: boolean + onClose: () => void + position?: { x: number; y: number } + onPositionChange?: (position: { x: number; y: number }) => void +} + +// --- 样式组件 --- +const Container = styled.div` + width: 300px; + background-color: var(--color-background); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + overflow: hidden; + display: flex; + flex-direction: column; + transform-origin: top left; + will-change: transform; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + cursor: default; +` + +const Header = styled.div` + padding: 8px 12px; + background-color: var(--color-primary); + color: white; + font-weight: bold; + display: flex; + align-items: center; + cursor: move; + user-select: none; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background-color: rgba(255, 255, 255, 0.2); + } + + &:hover::before { + background-color: rgba(255, 255, 255, 0.4); + } + + .drag-icon { + margin-right: 8px; // DragOutlined 的样式 + } + + .settings-button { + margin-left: auto; // 推到最右边 + color: white; // 设置按钮颜色 + } +` + +const CloseButton = styled.div` + margin-left: 8px; // 与设置按钮保持间距 + cursor: pointer; +` + +const Content = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; +` + +const VisualizerContainer = styled.div` + display: flex; + justify-content: space-between; + height: 60px; +` + +const TranscriptContainer = styled.div` + flex: 1; + min-height: 60px; + max-height: 100px; + overflow-y: auto; + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 8px; + background-color: var(--color-background-2); +` + +const TranscriptText = styled.div` + margin-bottom: 8px; +` + +const UserLabel = styled.span` + font-weight: bold; + color: var(--color-primary); +` + +const ControlsContainer = styled.div` + display: flex; + justify-content: center; + padding: 8px 0; +` + +const RecordButton = styled(Button)` + min-width: 120px; +` + +// 设置面板的样式 +const SettingsPanel = styled.div` + margin-bottom: 10px; + padding: 10px; + border: 1px solid var(--color-border); + border-radius: 8px; +` + +const SettingsTitle = styled.div` + margin-bottom: 8px; +` + +const ShortcutKeyButton = styled(Button)` + min-width: 120px; +` + +const SettingsTip = styled.div` + margin-top: 8px; + font-size: 12px; + color: var(--color-text-secondary); +` +// --- 样式组件结束 --- + +const DraggableVoiceCallWindow: React.FC = ({ + visible, + onClose, + position = { x: 20, y: 20 }, + onPositionChange +}) => { + const { t } = useTranslation() + const dispatch = useDispatch() + const [isDragging, setIsDragging] = useState(false) + const [currentPosition, setCurrentPosition] = useState(position) + const dragStartRef = useRef<{ startX: number; startY: number; initialX: number; initialY: number } | null>(null) + const containerRef = useRef(null) + + // --- 语音通话状态 --- + const [transcript, setTranscript] = useState('') + const [isListening, setIsListening] = useState(false) + const [isSpeaking, setIsSpeaking] = useState(false) + const [isRecording, setIsRecording] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [isMuted, setIsMuted] = useState(false) + // --- 语音通话状态结束 --- + + // --- 快捷键相关状态 --- + const [shortcutKey, setShortcutKey] = useState('Space') + const [isShortcutPressed, setIsShortcutPressed] = useState(false) + const [isSettingsVisible, setIsSettingsVisible] = useState(false) + const [tempShortcutKey, setTempShortcutKey] = useState(shortcutKey) + const [isRecordingShortcut, setIsRecordingShortcut] = useState(false) + const [isCollapsed, setIsCollapsed] = useState(false) + // --- 快捷键相关状态结束 --- + + const isInitializedRef = useRef(false) + + // --- 拖拽逻辑 --- + const handleDragStart = useCallback( + (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest('button, input, a')) { + return + } + e.preventDefault() + setIsDragging(true) + dragStartRef.current = { + startX: e.clientX, + startY: e.clientY, + initialX: currentPosition.x, + initialY: currentPosition.y + } + }, + [currentPosition] + ) + + const handleDrag = useCallback( + (e: MouseEvent) => { + if (!isDragging || !dragStartRef.current) return + e.preventDefault() + + const deltaX = e.clientX - dragStartRef.current.startX + const deltaY = e.clientY - dragStartRef.current.startY + + let newX = dragStartRef.current.initialX + deltaX + let newY = dragStartRef.current.initialY + deltaY + + const windowWidth = window.innerWidth + const windowHeight = window.innerHeight + const containerWidth = containerRef.current?.offsetWidth || 300 + const containerHeight = containerRef.current?.offsetHeight || 300 + + newX = Math.max(0, Math.min(newX, windowWidth - containerWidth)) + newY = Math.max(0, Math.min(newY, windowHeight - containerHeight)) + + const newPosition = { x: newX, y: newY } + setCurrentPosition(newPosition) + onPositionChange?.(newPosition) + }, + [isDragging, onPositionChange] + ) + + const handleDragEnd = useCallback( + (e: MouseEvent) => { + if (isDragging) { + e.preventDefault() + setIsDragging(false) + dragStartRef.current = null + } + }, + [isDragging] // 移除了 currentPosition 依赖,因为它只在 handleDragStart 中读取一次 + ) + + const throttle = useMemo(() => { + let lastCall = 0 + const delay = 16 // ~60fps + return (func: (e: MouseEvent) => void) => { + return (e: MouseEvent) => { + const now = new Date().getTime() + if (now - lastCall < delay) { + return + } + lastCall = now + func(e) + } + } + }, []) + + const throttledHandleDrag = useMemo(() => throttle(handleDrag), [handleDrag, throttle]) + + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', throttledHandleDrag) + document.addEventListener('mouseup', handleDragEnd) + document.body.style.cursor = 'move' + } else { + document.removeEventListener('mousemove', throttledHandleDrag) + document.removeEventListener('mouseup', handleDragEnd) + document.body.style.cursor = 'default' + } + return () => { + document.removeEventListener('mousemove', throttledHandleDrag) + document.removeEventListener('mouseup', handleDragEnd) + document.body.style.cursor = 'default' + } + }, [isDragging, throttledHandleDrag, handleDragEnd]) + // --- 拖拽逻辑结束 --- + + // --- 状态和副作用管理 --- + useEffect(() => { + const handleTTSStateChange = (event: CustomEvent) => { + const { isPlaying } = event.detail + setIsSpeaking(isPlaying) + } + + const startVoiceCall = async () => { + try { + window.message.loading({ content: t('voice_call.initializing'), key: 'voice-call-init' }) + try { + await VoiceCallService.initialize() + } catch (initError) { + console.warn('语音识别服务初始化警告:', initError) + } + await VoiceCallService.startCall({ + onTranscript: setTranscript, + onResponse: () => { + /* 响应在聊天界面处理 */ + }, + onListeningStateChange: setIsListening, + onSpeakingStateChange: setIsSpeaking + }) + window.message.success({ content: t('voice_call.ready'), key: 'voice-call-init' }) + isInitializedRef.current = true + } catch (error) { + console.error('Voice call error:', error) + window.message.error({ content: t('voice_call.error'), key: 'voice-call-init' }) + onClose() + } + } + + if (visible) { + dispatch(setIsVoiceCallActive(true)) + dispatch(setLastPlayedMessageId(null)) + dispatch(setSkipNextAutoTTS(true)) + if (!isInitializedRef.current) { + startVoiceCall() + } + window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) + } else if (!visible && isInitializedRef.current) { + dispatch(setIsVoiceCallActive(false)) + dispatch(setSkipNextAutoTTS(false)) + VoiceCallService.endCall() + setTranscript('') + setIsListening(false) + setIsSpeaking(false) + setIsRecording(false) + setIsProcessing(false) + setIsPaused(false) + setIsMuted(false) + isInitializedRef.current = false + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) + } + + return () => { + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) + } + }, [visible, dispatch, t, onClose]) + // --- 状态和副作用管理结束 --- + + // --- 语音通话控制函数 --- + const toggleMute = useCallback(() => { + const newMuteState = !isMuted + setIsMuted(newMuteState) + VoiceCallService.setMuted(newMuteState) + }, [isMuted]) // 添加依赖 + + const togglePause = useCallback(() => { + const newPauseState = !isPaused + setIsPaused(newPauseState) + VoiceCallService.setPaused(newPauseState) + }, [isPaused]) // 添加依赖 + + // !! 将这些函数定义移到 handleKeyDown/handleKeyUp 之前 !! + const handleRecordStart = useCallback( + async (e: React.MouseEvent | React.TouchEvent | KeyboardEvent) => { + e.preventDefault() + if (isProcessing || isPaused) return + setTranscript('') + VoiceCallService.stopTTS() + setIsSpeaking(false) + setIsRecording(true) + setIsProcessing(true) + try { + await VoiceCallService.startRecording() + setIsProcessing(false) + } catch (error) { + window.message.error({ content: '启动语音识别失败,请确保语音识别服务已启动', key: 'voice-call-error' }) + setIsRecording(false) + setIsProcessing(false) + } + }, + [isProcessing, isPaused] + ) + + const handleRecordEnd = useCallback( + async (e: React.MouseEvent | React.TouchEvent | KeyboardEvent) => { + e.preventDefault() + if (!isRecording) return + setIsRecording(false) + setIsProcessing(true) + VoiceCallService.stopTTS() + setIsSpeaking(false) + try { + const success = await VoiceCallService.stopRecordingAndSendToChat() + if (success) { + window.message.success({ content: '语音识别已完成,正在发送消息...', key: 'voice-call-send' }) + } else { + window.message.error({ content: '发送语音识别结果失败', key: 'voice-call-error' }) + } + } catch (error) { + window.message.error({ content: '停止录音出错', key: 'voice-call-error' }) + } finally { + setTimeout(() => setIsProcessing(false), 500) + } + }, + [isRecording] + ) + + const handleRecordCancel = useCallback( + async (e: React.MouseEvent | React.TouchEvent | KeyboardEvent) => { + e.preventDefault() + if (isRecording) { + setIsRecording(false) + setIsProcessing(true) + VoiceCallService.stopTTS() + setIsSpeaking(false) + try { + await VoiceCallService.cancelRecording() + setTranscript('') + } catch (error) { + console.error('取消录音出错:', error) + } finally { + setTimeout(() => setIsProcessing(false), 500) + } + } + }, + [isRecording] + ) + // --- 语音通话控制函数结束 --- + + // --- 快捷键相关函数 --- + const getKeyDisplayName = (keyCode: string) => { + const keyMap: Record = { + Space: '空格键', + Enter: '回车键', + ShiftLeft: '左Shift键', + ShiftRight: '右Shift键', + ControlLeft: '左Ctrl键', + ControlRight: '右Ctrl键', + AltLeft: '左Alt键', + AltRight: '右Alt键' + } + return keyMap[keyCode] || keyCode + } + + const handleShortcutKeyChange = useCallback( + (e: KeyboardEvent) => { + e.preventDefault() + if (isRecordingShortcut) { + setTempShortcutKey(e.code) + setIsRecordingShortcut(false) + } + }, + [isRecordingShortcut] + ) + + const saveShortcutKey = useCallback(() => { + setShortcutKey(tempShortcutKey) + localStorage.setItem('voiceCallShortcutKey', tempShortcutKey) + setIsSettingsVisible(false) + }, [tempShortcutKey]) + + // 现在可以安全地使用 handleRecordStart/End + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (isRecordingShortcut) { + handleShortcutKeyChange(e) + return + } + if (e.code === shortcutKey && !isProcessing && !isPaused && visible && !isShortcutPressed) { + e.preventDefault() + setIsShortcutPressed(true) + const mockEvent = new MouseEvent('mousedown') as unknown as React.MouseEvent // 类型断言 + handleRecordStart(mockEvent) // 现在 handleRecordStart 已经定义 + } + }, + [ + shortcutKey, + isProcessing, + isPaused, + visible, + isShortcutPressed, + handleRecordStart, // 依赖项 + isRecordingShortcut, + handleShortcutKeyChange + ] + ) + + const handleKeyUp = useCallback( + (e: KeyboardEvent) => { + if (e.code === shortcutKey && isShortcutPressed && visible) { + e.preventDefault() + setIsShortcutPressed(false) + const mockEvent = new MouseEvent('mouseup') as unknown as React.MouseEvent // 类型断言 + handleRecordEnd(mockEvent) // 现在 handleRecordEnd 已经定义 + } + }, + [shortcutKey, isShortcutPressed, visible, handleRecordEnd] + ) // 依赖项 + + useEffect(() => { + const savedShortcut = localStorage.getItem('voiceCallShortcutKey') + if (savedShortcut) { + setShortcutKey(savedShortcut) + setTempShortcutKey(savedShortcut) + } + }, []) + + useEffect(() => { + if (visible) { + window.addEventListener('keydown', handleKeyDown) + window.addEventListener('keyup', handleKeyUp) + } + return () => { + window.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('keyup', handleKeyUp) + } + }, [visible, handleKeyDown, handleKeyUp]) + // --- 快捷键相关函数结束 --- + + // 如果不可见,直接返回 null + if (!visible) return null + + // --- JSX 渲染 --- + return ( + + {/* 将 onMouseDown 移到 Header 上 */} +
+ {/* 应用样式类 */} + {t('voice_call.title')} +
+ + + {!isCollapsed && ( + <> + {isSettingsVisible && ( + + {' '} + {/* 使用 styled-component */} + {t('voice_call.shortcut_key_setting')} {/* 使用 styled-component */} + + setIsRecordingShortcut(true)}> + {' '} + {/* 使用 styled-component */} + {isRecordingShortcut ? t('voice_call.press_any_key') : getKeyDisplayName(tempShortcutKey)} + + + + + + {' '} + {/* 使用 styled-component */} + {t('voice_call.shortcut_key_tip')} + + + )} + + + + + + + {transcript && ( + + {t('voice_call.you')}: {transcript} + + )} + {/* 可以在这里添加 AI 回复的显示 */} + + + )} + + + + + + + +
+ ) +} + +const Container = styled.div` + padding: 0 0 20px 0; +` + +const Tooltip = styled.div` + position: relative; + display: inline-block; + cursor: help; + + &:hover::after { + content: attr(title); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: 5px 10px; + background-color: var(--color-background-soft); + border: 1px solid var(--color-border); + border-radius: 4px; + white-space: nowrap; + z-index: 1; + font-size: 12px; + } +` + +const Alert = styled.div<{ type: 'info' | 'warning' | 'error' | 'success' }>` + padding: 10px 15px; + border-radius: 4px; + margin-bottom: 16px; + background-color: ${(props) => + props.type === 'info' + ? 'var(--color-info-bg)' + : props.type === 'warning' + ? 'var(--color-warning-bg)' + : props.type === 'error' + ? 'var(--color-error-bg)' + : 'var(--color-success-bg)'}; + border: 1px solid + ${(props) => + props.type === 'info' + ? 'var(--color-info-border)' + : props.type === 'warning' + ? 'var(--color-warning-border)' + : props.type === 'error' + ? 'var(--color-error-border)' + : 'var(--color-success-border)'}; + color: ${(props) => + props.type === 'info' + ? 'var(--color-info-text)' + : props.type === 'warning' + ? 'var(--color-warning-text)' + : props.type === 'error' + ? 'var(--color-error-text)' + : 'var(--color-success-text)'}; +` + +const BrowserTip = styled.div` + font-size: 12px; + color: var(--color-text-3); + margin-top: 8px; +` + +export default ASRSettings diff --git a/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx new file mode 100644 index 0000000000..a45f8ed69c --- /dev/null +++ b/src/renderer/src/pages/settings/TTSSettings/TTSSettings.tsx @@ -0,0 +1,1058 @@ +import { AudioOutlined, PhoneOutlined, PlusOutlined, ReloadOutlined, SoundOutlined } from '@ant-design/icons' +import { useTheme } from '@renderer/context/ThemeProvider' +import TTSService from '@renderer/services/TTSService' +import store, { useAppDispatch } from '@renderer/store' +import { + addTtsCustomModel, + addTtsCustomVoice, + removeTtsCustomModel, + removeTtsCustomVoice, + resetTtsCustomValues, + setShowTTSProgressBar, + setTtsApiKey, + setTtsApiUrl, + setTtsEdgeVoice, + setTtsEnabled, + setTtsFilterOptions, + setTtsModel, + setTtsMsOutputFormat, + setTtsMsVoice, + setTtsServiceType, + setTtsSiliconflowApiKey, + setTtsSiliconflowApiUrl, + setTtsSiliconflowModel, + setTtsSiliconflowResponseFormat, + setTtsSiliconflowSpeed, + setTtsSiliconflowVoice, + setTtsVoice +} from '@renderer/store/settings' +import { Button, Form, Input, InputNumber, message, Select, Space, Switch, Tabs, Tag } from 'antd' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import { + SettingContainer, + SettingDivider, + SettingGroup, + SettingHelpText, + SettingRow, + SettingRowTitle, + SettingTitle +} from '..' +import ASRSettings from './ASRSettings' +import VoiceCallSettings from './VoiceCallSettings' + +// 预定义的浏览器 TTS音色列表 +const PREDEFINED_VOICES = [ + { label: '小晓 (女声, 中文)', value: 'zh-CN-XiaoxiaoNeural' }, + { label: '云扬 (男声, 中文)', value: 'zh-CN-YunyangNeural' }, + { label: '晓晓 (女声, 中文)', value: 'zh-CN-XiaoxiaoNeural' }, + { label: '晓涵 (女声, 中文)', value: 'zh-CN-XiaohanNeural' }, + { label: '晓诗 (女声, 中文)', value: 'zh-CN-XiaoshuangNeural' }, + { label: '晓瑞 (女声, 中文)', value: 'zh-CN-XiaoruiNeural' }, + { label: '晓墨 (女声, 中文)', value: 'zh-CN-XiaomoNeural' }, + { label: '晓然 (男声, 中文)', value: 'zh-CN-XiaoranNeural' }, + { label: '晓坤 (男声, 中文)', value: 'zh-CN-XiaokunNeural' }, + { label: 'Aria (Female, English)', value: 'en-US-AriaNeural' }, + { label: 'Guy (Male, English)', value: 'en-US-GuyNeural' }, + { label: 'Jenny (Female, English)', value: 'en-US-JennyNeural' }, + { label: 'Ana (Female, Spanish)', value: 'es-ES-ElviraNeural' }, + { label: 'Ichiro (Male, Japanese)', value: 'ja-JP-KeitaNeural' }, + { label: 'Nanami (Female, Japanese)', value: 'ja-JP-NanamiNeural' }, + // 添加更多常用的语音 + { label: 'Microsoft David (en-US)', value: 'Microsoft David Desktop - English (United States)' }, + { label: 'Microsoft Zira (en-US)', value: 'Microsoft Zira Desktop - English (United States)' }, + { label: 'Microsoft Mark (en-US)', value: 'Microsoft Mark Online (Natural) - English (United States)' }, + { label: 'Microsoft Aria (en-US)', value: 'Microsoft Aria Online (Natural) - English (United States)' }, + { label: 'Google US English', value: 'Google US English' }, + { label: 'Google UK English Female', value: 'Google UK English Female' }, + { label: 'Google UK English Male', value: 'Google UK English Male' }, + { label: 'Google 日本語', value: 'Google 日本語' }, + { label: 'Google 普通话(中国大陆)', value: 'Google 普通话(中国大陆)' }, + { label: 'Google 粤語(香港)', value: 'Google 粤語(香港)' } +] + +const CustomVoiceInput = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +` + +const TagsContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; + margin-bottom: 16px; +` + +const EmptyText = styled.div` + color: rgba(0, 0, 0, 0.45); + padding: 4px 0; +` + +const InputGroup = styled.div` + display: flex; + gap: 8px; + margin-bottom: 8px; +` + +const FlexContainer = styled.div` + display: flex; + gap: 8px; +` + +const FilterOptionItem = styled.div` + margin-bottom: 16px; + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +` + +const LengthLabel = styled.span` + margin-right: 8px; +` + +const LoadingText = styled.div` + margin-top: 8px; + color: #999; +` + +const InfoText = styled.div` + margin-top: 8px; + font-size: 12px; + color: #888; +` + +const VoiceSelectContainer = styled.div` + display: flex; + gap: 8px; + margin-bottom: 8px; +` + +const TTSSettings: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + + // 从Redux获取TTS设置 + const settings = useSelector((state: any) => state.settings) + const ttsEnabled = settings.ttsEnabled + const ttsServiceType = settings.ttsServiceType || 'openai' + const ttsApiKey = settings.ttsApiKey + const ttsApiUrl = settings.ttsApiUrl + const ttsVoice = settings.ttsVoice + const ttsModel = settings.ttsModel + const ttsEdgeVoice = settings.ttsEdgeVoice || 'zh-CN-XiaoxiaoNeural' + const ttsCustomVoices = settings.ttsCustomVoices || [] + const ttsCustomModels = settings.ttsCustomModels || [] + const showTTSProgressBar = settings.showTTSProgressBar + // 免费在线TTS设置 + const ttsMsVoice = settings.ttsMsVoice || 'zh-CN-XiaoxiaoNeural' + const ttsMsOutputFormat = settings.ttsMsOutputFormat || 'audio-24khz-48kbitrate-mono-mp3' + + // 确保免费在线TTS设置有默认值 + useEffect(() => { + if (ttsServiceType === 'mstts') { + if (!settings.ttsMsVoice) { + dispatch(setTtsMsVoice('zh-CN-XiaoxiaoNeural')) + } + if (!settings.ttsMsOutputFormat) { + dispatch(setTtsMsOutputFormat('audio-24khz-48kbitrate-mono-mp3')) + } + } + }, [ttsServiceType, settings.ttsMsVoice, settings.ttsMsOutputFormat, dispatch]) + const ttsFilterOptions = settings.ttsFilterOptions || { + filterThinkingProcess: true, + filterMarkdown: true, + filterCodeBlocks: true, + filterHtmlTags: true, + filterEmojis: true, + maxTextLength: 4000 + } + + // 硅基流动TTS设置 + const ttsSiliconflowApiKey = settings.ttsSiliconflowApiKey + const ttsSiliconflowApiUrl = settings.ttsSiliconflowApiUrl + const ttsSiliconflowVoice = settings.ttsSiliconflowVoice + const ttsSiliconflowModel = settings.ttsSiliconflowModel + const ttsSiliconflowResponseFormat = settings.ttsSiliconflowResponseFormat + const ttsSiliconflowSpeed = settings.ttsSiliconflowSpeed + + // 新增自定义音色和模型的状态 + const [newVoice, setNewVoice] = useState('') + const [newModel, setNewModel] = useState('') + + // 浏览器可用的语音列表 + const [availableVoices, setAvailableVoices] = useState<{ label: string; value: string }[]>([]) + + // 免费在线TTS可用的语音列表 + const [msTtsVoices, setMsTtsVoices] = useState<{ label: string; value: string }[]>([]) + + // 获取免费在线TTS可用的语音列表 + const getMsTtsVoices = useCallback(async () => { + try { + // 调用API获取免费在线TTS语音列表 + const response = await window.api.msTTS.getVoices() + console.log('获取到的免费在线TTS语音列表:', response) + + // 转换为选项格式 + const voices = response.map((voice: any) => ({ + label: `${voice.ShortName} (${voice.Gender === 'Female' ? '女声' : '男声'})`, + value: voice.ShortName + })) + + // 按语言和性别排序 + voices.sort((a: any, b: any) => { + const localeA = a.value.split('-')[0] + a.value.split('-')[1] + const localeB = b.value.split('-')[0] + b.value.split('-')[1] + if (localeA !== localeB) return localeA.localeCompare(localeB) + return a.label.localeCompare(b.label) + }) + + setMsTtsVoices(voices) + } catch (error) { + console.error('获取免费在线TTS语音列表失败:', error) + // 如果获取失败,设置一些默认的中文语音 + setMsTtsVoices([ + { label: 'zh-CN-XiaoxiaoNeural (女声)', value: 'zh-CN-XiaoxiaoNeural' }, + { label: 'zh-CN-YunxiNeural (男声)', value: 'zh-CN-YunxiNeural' }, + { label: 'zh-CN-YunyangNeural (男声)', value: 'zh-CN-YunyangNeural' }, + { label: 'zh-CN-XiaohanNeural (女声)', value: 'zh-CN-XiaohanNeural' }, + { label: 'zh-CN-XiaomoNeural (女声)', value: 'zh-CN-XiaomoNeural' }, + { label: 'zh-CN-XiaoxuanNeural (女声)', value: 'zh-CN-XiaoxuanNeural' }, + { label: 'zh-CN-XiaoruiNeural (女声)', value: 'zh-CN-XiaoruiNeural' }, + { label: 'zh-CN-YunfengNeural (男声)', value: 'zh-CN-YunfengNeural' } + ]) + } + }, []) + + // 获取浏览器可用的语音列表 + const getVoices = useCallback(() => { + if (typeof window !== 'undefined' && 'speechSynthesis' in window) { + // 先触发一下语音合成引擎,确保它已经初始化 + window.speechSynthesis.cancel() + + // 获取浏览器可用的语音列表 + const voices = window.speechSynthesis.getVoices() + console.log('获取到的语音列表:', voices) + console.log('语音列表长度:', voices.length) + + // 转换浏览器语音列表为选项格式 + const browserVoices = voices.map((voice) => ({ + label: `${voice.name} (${voice.lang})${voice.default ? ' - 默认' : ''}`, + value: voice.name, + lang: voice.lang, + isNative: true // 标记为浏览器原生语音 + })) + + // 添加语言信息到预定义语音 + const enhancedPredefinedVoices = PREDEFINED_VOICES.map((voice) => ({ + ...voice, + lang: voice.value.split('-').slice(0, 2).join('-'), + isNative: false // 标记为非浏览器原生语音 + })) + + // 合并所有语音列表 + // 只使用浏览器原生语音,因为预定义语音实际不可用 + let allVoices = [...browserVoices] + + // 如果浏览器没有可用语音,才使用预定义语音 + if (browserVoices.length === 0) { + allVoices = [...enhancedPredefinedVoices] + console.log('浏览器没有可用语音,使用预定义语音') + } else { + console.log('使用浏览器原生语音,共' + browserVoices.length + '个') + } + + // 去除重复项,优先保留浏览器原生语音 + const uniqueVoices = allVoices.filter((voice, index, self) => { + const firstIndex = self.findIndex((v) => v.value === voice.value) + // 如果是原生语音或者是第一次出现,则保留 + return voice.isNative || firstIndex === index + }) + + // 按语言分组并排序 + const groupedVoices = uniqueVoices.sort((a, b) => { + // 先按语言排序 + if (a.lang !== b.lang) { + return a.lang.localeCompare(b.lang) + } + // 同语言下,原生语音优先 + if (a.isNative !== b.isNative) { + return a.isNative ? -1 : 1 + } + // 最后按名称排序 + return a.label.localeCompare(b.label) + }) + + setAvailableVoices(groupedVoices) + console.log('设置可用语音列表:', groupedVoices) + } else { + // 如果浏览器不支持Web Speech API,使用预定义的语音列表 + console.log('浏览器不支持Web Speech API,使用预定义的语音列表') + setAvailableVoices(PREDEFINED_VOICES) + } + }, []) + + // 刷新语音列表 + const refreshVoices = useCallback(() => { + console.log('手动刷新语音列表') + message.loading({ + content: t('settings.tts.edge_voice.refreshing', { defaultValue: '正在刷新语音列表...' }), + key: 'refresh-voices' + }) + + // 先清空当前列表 + setAvailableVoices([]) + + // 强制重新加载语音列表 + if (typeof window !== 'undefined' && 'speechSynthesis' in window) { + window.speechSynthesis.cancel() + + // 尝试多次获取语音列表 + setTimeout(() => { + getVoices() + setTimeout(() => { + getVoices() + message.success({ + content: t('settings.tts.edge_voice.refreshed', { defaultValue: '语音列表已刷新' }), + key: 'refresh-voices' + }) + }, 1000) + }, 500) + } else { + // 如果浏览器不支持Web Speech API,使用预定义的语音列表 + setAvailableVoices(PREDEFINED_VOICES) + message.success({ + content: t('settings.tts.edge_voice.refreshed', { defaultValue: '语音列表已刷新' }), + key: 'refresh-voices' + }) + } + }, [getVoices, t]) + + // 获取免费在线TTS语音列表 + useEffect(() => { + // 获取免费在线TTS语音列表 + getMsTtsVoices() + }, [getMsTtsVoices]) + + useEffect(() => { + // 初始化语音合成引擎 + if (typeof window !== 'undefined' && 'speechSynthesis' in window) { + // 触发语音合成引擎初始化 + window.speechSynthesis.cancel() + + // 设置voiceschanged事件处理程序 + const voicesChangedHandler = () => { + console.log('检测到voiceschanged事件,重新获取语音列表') + getVoices() + } + + // 添加事件监听器 + window.speechSynthesis.onvoiceschanged = voicesChangedHandler + + // 立即获取可用的语音 + getVoices() + + // 创建多个定时器,在不同时间点尝试获取语音列表 + // 这是因为不同浏览器加载语音列表的时间不同 + const timers = [ + setTimeout(() => getVoices(), 500), + setTimeout(() => getVoices(), 1000), + setTimeout(() => getVoices(), 2000) + ] + + return () => { + // 清理事件监听器和定时器 + window.speechSynthesis.onvoiceschanged = null + timers.forEach((timer) => clearTimeout(timer)) + } + } else { + // 如果浏览器不支持Web Speech API,使用预定义的语音列表 + setAvailableVoices(PREDEFINED_VOICES) + return () => {} + } + }, [getVoices]) + + // 测试TTS功能 + const testTTS = async () => { + if (!ttsEnabled) { + window.message.error({ content: t('settings.tts.error.not_enabled'), key: 'tts-test' }) + return + } + + // 如果是免费在线TTS,确保音色已设置 + if (ttsServiceType === 'mstts' && !ttsMsVoice) { + // 自动设置默认音色 + dispatch(setTtsMsVoice('zh-CN-XiaoxiaoNeural')) + window.message.info({ + content: t('settings.tts.mstts.auto_set_voice', { defaultValue: '已自动设置默认音色' }), + key: 'tts-test' + }) + } + + // 强制刷新状态,确保使用最新的设置 + // 先获取当前的服务类型 + const currentType = store.getState().settings.ttsServiceType || 'openai' + console.log('测试前当前的TTS服务类型:', currentType) + + // 获取最新的服务类型设置 + const latestSettings = store.getState().settings + const currentServiceType = latestSettings.ttsServiceType || 'openai' + console.log('测试TTS时使用的服务类型:', currentServiceType) + console.log('测试时完整TTS设置:', { + ttsEnabled: latestSettings.ttsEnabled, + ttsServiceType: latestSettings.ttsServiceType, + ttsApiKey: latestSettings.ttsApiKey ? '已设置' : '未设置', + ttsVoice: latestSettings.ttsVoice, + ttsModel: latestSettings.ttsModel, + ttsEdgeVoice: latestSettings.ttsEdgeVoice, + ttsSiliconflowApiKey: latestSettings.ttsSiliconflowApiKey ? '已设置' : '未设置', + ttsSiliconflowVoice: latestSettings.ttsSiliconflowVoice, + ttsSiliconflowModel: latestSettings.ttsSiliconflowModel, + ttsSiliconflowResponseFormat: latestSettings.ttsSiliconflowResponseFormat, + ttsSiliconflowSpeed: latestSettings.ttsSiliconflowSpeed + }) + + // 根据服务类型检查必要的参数 + if (currentServiceType === 'openai') { + if (!ttsApiKey) { + window.message.error({ content: t('settings.tts.error.no_api_key'), key: 'tts-test' }) + return + } + + if (!ttsVoice) { + window.message.error({ content: t('settings.tts.error.no_voice'), key: 'tts-test' }) + return + } + + if (!ttsModel) { + window.message.error({ content: t('settings.tts.error.no_model'), key: 'tts-test' }) + return + } + } else if (currentServiceType === 'edge') { + if (!ttsEdgeVoice) { + window.message.error({ content: t('settings.tts.error.no_edge_voice'), key: 'tts-test' }) + return + } + } else if (currentServiceType === 'siliconflow') { + const ttsSiliconflowApiKey = latestSettings.ttsSiliconflowApiKey + const ttsSiliconflowVoice = latestSettings.ttsSiliconflowVoice + const ttsSiliconflowModel = latestSettings.ttsSiliconflowModel + + if (!ttsSiliconflowApiKey) { + window.message.error({ content: t('settings.tts.error.no_api_key'), key: 'tts-test' }) + return + } + + if (!ttsSiliconflowVoice) { + window.message.error({ content: t('settings.tts.error.no_voice'), key: 'tts-test' }) + return + } + + if (!ttsSiliconflowModel) { + window.message.error({ content: t('settings.tts.error.no_model'), key: 'tts-test' }) + return + } + } + + await TTSService.speak('这是一段测试语音,用于测试TTS功能是否正常工作。') + } + + // 添加自定义音色 + const handleAddVoice = () => { + if (!newVoice) { + window.message.error({ content: '请输入音色', key: 'add-voice' }) + return + } + + // 确保添加的是字符串 + const voiceStr = typeof newVoice === 'string' ? newVoice : String(newVoice) + dispatch(addTtsCustomVoice(voiceStr)) + setNewVoice('') + } + + // 添加自定义模型 + const handleAddModel = () => { + if (!newModel) { + window.message.error({ content: '请输入模型', key: 'add-model' }) + return + } + + // 确保添加的是字符串 + const modelStr = typeof newModel === 'string' ? newModel : String(newModel) + dispatch(addTtsCustomModel(modelStr)) + setNewModel('') + } + + // 删除自定义音色 + const handleRemoveVoice = (voice: string) => { + // 确保删除的是字符串 + const voiceStr = typeof voice === 'string' ? voice : String(voice) + dispatch(removeTtsCustomVoice(voiceStr)) + } + + // 删除自定义模型 + const handleRemoveModel = (model: string) => { + // 确保删除的是字符串 + const modelStr = typeof model === 'string' ? model : String(model) + dispatch(removeTtsCustomModel(modelStr)) + } + + return ( + + + + + {t('settings.voice.title')} + + + + + {t('settings.tts.tab_title')} + + ), + children: ( +
+ + + {t('settings.tts.enable')} + dispatch(setTtsEnabled(checked))} /> + + {t('settings.tts.enable.help')} + + + {/* 重置按钮 */} + + + {t('settings.tts.reset_title')} + + + {t('settings.tts.reset_help')} + + + {t('settings.tts.api_settings')} +
+ {/* TTS服务类型选择 */} + + + dispatch(setTtsApiUrl(e.target.value))} + placeholder={t('settings.tts.api_url.placeholder')} + disabled={!ttsEnabled} + /> + + + )} + + {/* 硅基流动 TTS设置 */} + {ttsServiceType === 'siliconflow' && ( + <> + + dispatch(setTtsSiliconflowApiKey(e.target.value))} + placeholder={t('settings.tts.siliconflow_api_key.placeholder')} + disabled={!ttsEnabled} + /> + + + dispatch(setTtsSiliconflowApiUrl(e.target.value))} + placeholder={t('settings.tts.siliconflow_api_url.placeholder')} + disabled={!ttsEnabled} + /> + + + dispatch(setTtsSiliconflowModel(value))} + options={[{ label: 'FunAudioLLM/CosyVoice2-0.5B', value: 'FunAudioLLM/CosyVoice2-0.5B' }]} + disabled={!ttsEnabled} + style={{ width: '100%' }} + placeholder={t('settings.tts.siliconflow_model.placeholder')} + showSearch + optionFilterProp="label" + allowClear + /> + + + dispatch(setTtsEdgeVoice(value))} + options={ + availableVoices.length > 0 + ? availableVoices + : [{ label: t('settings.tts.edge_voice.loading'), value: '' }] + } + disabled={!ttsEnabled} + style={{ flex: 1 }} + showSearch + optionFilterProp="label" + placeholder={ + availableVoices.length === 0 + ? t('settings.tts.edge_voice.loading') + : t('settings.tts.voice.placeholder') + } + notFoundContent={ + availableVoices.length === 0 + ? t('settings.tts.edge_voice.loading') + : t('settings.tts.edge_voice.not_found') + } + /> + + + + + {/* 模型选择 */} + + setNewModel(e.target.value)} + disabled={!ttsEnabled} + style={{ flex: 1 }} + /> + + + + + )} + + {/* TTS过滤选项 */} + + + dispatch(setShowTTSProgressBar(checked))} + disabled={!ttsEnabled} + />{' '} + {t('settings.tts.show_progress_bar', { defaultValue: '显示TTS进度条' })} + + + dispatch(setTtsFilterOptions({ filterThinkingProcess: checked }))} + disabled={!ttsEnabled} + />{' '} + {t('settings.tts.filter.thinking_process')} + + + dispatch(setTtsFilterOptions({ filterMarkdown: checked }))} + disabled={!ttsEnabled} + />{' '} + {t('settings.tts.filter.markdown')} + + + dispatch(setTtsFilterOptions({ filterCodeBlocks: checked }))} + disabled={!ttsEnabled} + />{' '} + {t('settings.tts.filter.code_blocks')} + + + dispatch(setTtsFilterOptions({ filterHtmlTags: checked }))} + disabled={!ttsEnabled} + />{' '} + {t('settings.tts.filter.html_tags')} + + + dispatch(setTtsFilterOptions({ filterEmojis: checked }))} + disabled={!ttsEnabled} + />{' '} + {t('settings.tts.filter.emojis', { defaultValue: '过滤表情符号' })} + + + {t('settings.tts.max_text_length')}: +