Compare commits

..

57 Commits

Author SHA1 Message Date
neru 20e162dc32 chore: remove unused param 2026-05-06 01:00:36 -03:00
neru 09e10e4113 fix: make v3 default 2026-02-13 22:48:32 -03:00
neru cbb5a9a76a fix: uncomment ShortName 2026-02-11 02:39:09 -03:00
neru 9025831f3d feat: use refreshtoken instead of api token 2026-02-11 02:37:02 -03:00
neru 06926e5601 style: run format:apply 2026-02-11 02:36:44 -03:00
neru 85c35021b5 style: run format:apply 2026-02-11 02:36:38 -03:00
neru c44f92f777 fix: dont use any 2026-02-11 02:36:33 -03:00
neru 1927728b60 fix: remove spammy log msg 2026-02-11 02:29:17 -03:00
neru 99b06b574b feat: refactor everything, handle websocket close 2026-02-09 04:49:59 -03:00
neru 7c3a5f6b56 fix: discard lib, implement azure tts 2026-02-08 02:27:02 -03:00
neru 27a6807340 fix: actually add mimic 😢 2026-02-06 14:12:22 -03:00
neru 2fe0551dee style: run format:apply and misc lint changes 2026-02-06 14:05:24 -03:00
neru 91a4c6e40d feat: add mimic 2026-02-06 14:04:57 -03:00
neru 69ee765889 feat: add bot category 2026-02-06 14:03:21 -03:00
neru d972e6598e fix: add missing env var 2026-02-02 09:35:47 -03:00
neru 2f4e944df4 fix: use env vars 2026-02-02 09:12:04 -03:00
neru 9571e32e61 feat: add script to dump firebase auth 2026-01-29 01:04:26 -03:00
neru b246afdc7f feat: add ElevenLabs Firebase token emulation 2026-01-29 01:04:19 -03:00
neru 123ed75b60 chore: add TTS_ELEVENLABS_TOKEN to dockerfile 2026-01-28 01:35:05 -03:00
neru 51ebb6c92d fix: implement tts_elevenlabs_token 2026-01-28 01:34:55 -03:00
neru 8e7a71164d fix: misc checks / changes 2026-01-28 01:34:32 -03:00
neru 224d1339e9 feat: add tts_elevenlabs_token 2026-01-28 01:33:59 -03:00
neru d9c623ac5c fix: getModels should return name not id 2026-01-23 22:36:25 -03:00
neru 68e622d318 fix: update docker-compose default vars 2026-01-23 14:07:15 -03:00
neru fea589dc2c fix: remove unused var 2026-01-23 14:06:13 -03:00
neru feabc732cf style: run lint and format 2026-01-23 14:03:45 -03:00
neru bfc749a034 fix: remove unneeded cast 2026-01-23 14:03:15 -03:00
neru 11539d149b fix: misc style / variable consistency changes 2026-01-23 14:03:02 -03:00
neru 7cbb5f3a9f feat: add ElevenLabs 2026-01-23 13:52:12 -03:00
neru f218a2cef9 style: run format:apply 2026-01-19 01:29:40 -03:00
neru 0cda7dd110 fix: disable generateSilence (unused) 2026-01-19 01:29:30 -03:00
neru ce98f13efd fix: remove unneeded call 2026-01-19 01:29:20 -03:00
neru 697cfd1de1 feat: check if msg is empty after filtering 2026-01-19 00:58:10 -03:00
neru f282a77411 feat: filter emotes, mentions and channels 2026-01-19 00:56:00 -03:00
neru 049897fb07 fix: undo changes 2026-01-17 20:54:30 -03:00
neru 17df430122 fix: ignore empty buffers 2026-01-17 20:53:11 -03:00
neru 042fde30c4 fix: refactor AudioMixer logic 2026-01-17 20:48:10 -03:00
neru c7ff5d3659 style: run format:apply 2026-01-15 19:26:56 -03:00
neru 893511ee11 fix: add fixed ms instead of percentage 2026-01-15 19:20:37 -03:00
neru 5bc4cd02ec feat: flush queue on tts-clear 2026-01-15 15:05:23 -03:00
neru 30966ec81a feat: add flush and stop methods 2026-01-15 15:05:16 -03:00
neru f7558913ee doc: important readme update 2026-01-15 14:40:01 -03:00
neru 6d21c3deca style: run format:apply 2026-01-15 14:36:33 -03:00
neru 14194d07ff fix: update default docker-compose 2026-01-15 14:14:57 -03:00
neru b3109d643d fix: add extra margin to avoid audios cutting off early 2026-01-15 13:14:30 -03:00
neru 00e02b9f97 fix: make database persistent 2026-01-15 13:00:31 -03:00
neru fd75f692d5 fix: cleanup streams on disconnection 2026-01-15 12:50:30 -03:00
neru e1363de9df fix: wrong check 2026-01-15 00:17:56 -03:00
neru 0fc38828be feat: add tiktok tts 2026-01-15 00:15:14 -03:00
neru c849c8ee11 feat: add owner_id bypass for debugging 2026-01-14 23:09:32 -03:00
neru 426c97e654 fix: add requiresAdmin to tts-channel 2026-01-14 23:08:47 -03:00
neru 60b66027a3 fix: add ffmpeg to docker image 2026-01-14 22:58:08 -03:00
neru fda4bd91aa style: fmt comment 2026-01-14 22:58:02 -03:00
neru 7b4dfb0dce fix: log transcoder errs 2026-01-14 22:57:55 -03:00
neru 8efdf0bc5b style: run format:apply 2026-01-14 22:52:53 -03:00
neru 449c4efbb7 fix: mod import validation 2026-01-14 22:52:02 -03:00
neru 4932bd18d3 chore: ignore docker-compose.yml 2026-01-14 22:40:29 -03:00
22 changed files with 1101 additions and 443 deletions
+2
View File
@@ -3,3 +3,5 @@ dist/*
db.sqlite db.sqlite
.env .env
docker-compose.yml
+1 -1
View File
@@ -6,5 +6,5 @@
another discord bot, but this one runs a lil bit better, maybe, hopefully another discord bot, but this one runs a lil bit better, maybe, hopefully
</p> </p>
<p>yeah idk look at this seal tho</p> <p>idk look at cute this seal tho</p>
<img src="https://i.pinimg.com/474x/16/eb/b9/16ebb902c9425b0d5a6251bbab048387.jpg"/> <img src="https://i.pinimg.com/474x/16/eb/b9/16ebb902c9425b0d5a6251bbab048387.jpg"/>
View File
+7 -3
View File
@@ -4,7 +4,11 @@ services:
container_name: luma container_name: luma
environment: environment:
NODE_ENV: production NODE_ENV: production
DISCORD_ID: DISCORD_ID: ${DISCORD_ID}
DISCORD_TOKEN: DISCORD_TOKEN: ${DISCORD_TOKEN}
TTS_AZURE_KEY: DISCORD_OWNER_ID: ${DISCORD_OWNER_ID}
TTS_TIKTOK_SESSIONID: ${TTS_TIKTOK_SESSIONID}
TTS_ELEVENLABS_REFRESHTOKEN: ${TTS_ELEVENLABS_REFRESHTOKEN}
restart: unless-stopped restart: unless-stopped
volumes:
- ./db.sqlite:/app/db.sqlite
+1 -1
View File
@@ -10,7 +10,7 @@ RUN npm run build
# prod # prod
FROM node:24-alpine FROM node:24-alpine
WORKDIR /app WORKDIR /app
RUN apk add --no-cache python3 make g++ RUN apk add --no-cache python3 make g++ ffmpeg
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only=production # only prod deps RUN npm ci --only=production # only prod deps
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
-358
View File
@@ -16,7 +16,6 @@
"colorts": "^0.1.63", "colorts": "^0.1.63",
"discord.js": "^14.25.1", "discord.js": "^14.25.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"edge-tts-universal": "^1.3.3",
"node-audio-mixer": "^2.1.0", "node-audio-mixer": "^2.1.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prism-media": "^1.3.5", "prism-media": "^1.3.5",
@@ -3042,23 +3041,6 @@
"dev": true, "dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3240,19 +3222,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/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/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3337,18 +3306,6 @@
"node": ">=8.1.90" "node": ">=8.1.90"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3361,15 +3318,6 @@
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/cross-fetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3433,15 +3381,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -3511,74 +3450,6 @@
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/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/edge-tts-universal": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/edge-tts-universal/-/edge-tts-universal-1.3.3.tgz",
"integrity": "sha512-jlUvYGsJ+o93FRPWQDQa/E7jqYbuLJAKKq9qvylo0/yHE1vtp4HCSzSAamviBewmpaNHlEJm+eEMimJXfu98zw==",
"license": "AGPL-3.0",
"dependencies": {
"axios": "^1.12.1",
"cross-fetch": "^4.1.0",
"https-proxy-agent": "^7.0.6",
"isomorphic-ws": "^5.0.0",
"uuid": "^11.1.0",
"ws": "^8.18.3",
"xml-escape": "^1.1.0"
},
"engines": {
"node": "^18.17 || ^20.9 || >=22",
"npm": ">=9"
}
},
"node_modules/edge-tts-universal/node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/edge-tts-universal/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/edge-tts-universal/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -3621,51 +3492,6 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.27.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
@@ -3997,42 +3823,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-constants": { "node_modules/fs-constants": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -4084,15 +3874,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": { "node_modules/gauge": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
@@ -4114,43 +3895,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/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.npmjs.org/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/get-tsconfig": { "node_modules/get-tsconfig": {
"version": "4.13.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
@@ -4230,18 +3974,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/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/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4259,51 +3991,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/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/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": { "node_modules/has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/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-cache-semantics": { "node_modules/http-cache-semantics": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -4524,15 +4217,6 @@
"devOptional": true, "devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -4730,36 +4414,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/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.npmjs.org/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/mimic-response": { "node_modules/mimic-response": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@@ -5493,12 +5147,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -6326,12 +5974,6 @@
} }
} }
}, },
"node_modules/xml-escape": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz",
"integrity": "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==",
"license": "MIT License"
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-1
View File
@@ -41,7 +41,6 @@
"colorts": "^0.1.63", "colorts": "^0.1.63",
"discord.js": "^14.25.1", "discord.js": "^14.25.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"edge-tts-universal": "^1.3.3",
"node-audio-mixer": "^2.1.0", "node-audio-mixer": "^2.1.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prism-media": "^1.3.5", "prism-media": "^1.3.5",
+10
View File
@@ -0,0 +1,10 @@
console.log('scanning localstorage')
const keys = Object.keys(localStorage).filter(k => k.startsWith("firebase:authUser"));
if (keys.length > 0) {
const data = JSON.parse(localStorage.getItem(keys[0]));
console.log("found in localstorage:", data.stsTokenManager);
} else {
console.error("no session found");
}
+4 -1
View File
@@ -255,7 +255,10 @@ export class CommandManager {
if (command.requiresAdmin) { if (command.requiresAdmin) {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.permissions.has(PermissionFlagsBits.Administrator)) { if (
!member.permissions.has(PermissionFlagsBits.Administrator) &&
member.id != config.owner_id
) {
await interaction.reply({ await interaction.reply({
content: content:
"You don't have the permissions required to execute this command.", "You don't have the permissions required to execute this command.",
+8
View File
@@ -0,0 +1,8 @@
import { CommandCategoryInfo } from '../../commands';
const info: CommandCategoryInfo = {
name: 'Bot',
description: 'Bot management commands'
};
export default info;
+100
View File
@@ -0,0 +1,100 @@
import {
ChatInputCommandInteraction,
MessageCreateOptions,
MessageFlags,
SlashCommandBuilder,
TextChannel
} from 'discord.js';
import { Command } from '../../commands';
const builder = new SlashCommandBuilder()
.setName('bot-mimic')
.setDescription('Makes the bot send a message')
.addStringOption((opt) =>
opt
.setName('content')
.setDescription('The text content of the message')
.setRequired(false)
)
.addAttachmentOption((opt) =>
opt
.setName('attachment')
.setDescription('An attachment for the message')
.setRequired(false)
)
.addStringOption((opt) =>
opt
.setName('reply')
.setDescription('The message ID that the bot should reply to')
.setRequired(false)
);
const command: Command = {
name: 'bot-mimic',
builder: builder,
ownerOnly: true,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.channel?.isTextBased()) {
await interaction.editReply(
'This command can only be used in a text channel.'
);
return;
}
if (!interaction.channel.isSendable()) {
await interaction.editReply('Channel is not sendable');
return;
}
const content = interaction.options.getString('content');
const attachment = interaction.options.getAttachment('attachment');
const replyId = interaction.options.getString('reply');
if (!content && !attachment) {
await interaction.editReply(
'Unable to send empty message. Specify content or attachment, or both.'
);
return;
}
const channel = interaction.channel as TextChannel;
const message: MessageCreateOptions = {};
if (content) {
message.content = content;
}
if (replyId) {
try {
const replyMessage = await channel.messages.fetch(replyId);
message.reply = {
messageReference: replyMessage.id
};
} catch {
await interaction.editReply('Invalid message ID for reply.');
return;
}
}
if (attachment) {
message.files = [
{
attachment: attachment.proxyURL,
name: attachment.name
}
];
}
try {
await channel.send(message);
await interaction.editReply('Message sent successfully.');
} catch (error) {
console.error('Failed to send message:', error);
await interaction.editReply('Failed to send message.');
}
}
};
export default command;
+1
View File
@@ -24,6 +24,7 @@ const cmd: Command = {
const queue = stream.getQueue('TTS'); const queue = stream.getQueue('TTS');
queue.clear(); queue.clear();
queue.flush();
interaction.reply('Queue cleared.'); interaction.reply('Queue cleared.');
} }
+118
View File
@@ -0,0 +1,118 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
SlashCommandBuilder
} from 'discord.js';
import { Command } from '../../commands';
import { TTSManager } from '../../modules/tts';
import { ElevenLabsTTS } from '../../modules/tts-modes/elevenlabs';
const builder = new SlashCommandBuilder()
.setName('elevenlabs-settings')
.setDescription('Configures ElevenLabs generation')
.addNumberOption((opt) =>
opt
.setName('stability')
.setDescription('Determines whether to be stable or more variable')
.setMaxValue(1)
.setMinValue(0)
)
.addNumberOption((opt) =>
opt
.setName('similarity-boost')
.setDescription('Boosts clarity and target voice similarity')
.setMaxValue(1.0)
.setMinValue(0)
)
.addNumberOption((opt) =>
opt
.setName('style')
.setDescription('How much should the style be exaggerated')
.setMaxValue(1.0)
.setMinValue(0)
)
.addNumberOption((opt) =>
opt
.setName('speed')
.setDescription('The speed at which the text should be read')
.setMaxValue(1.2)
.setMinValue(0.7)
)
.addBooleanOption((opt) =>
opt
.setName('speaker-boost')
.setDescription('Should speaker boost be enabled?')
)
.addStringOption((opt) =>
opt
.setName('model')
.setDescription('Which generation model to use')
.setAutocomplete(true)
);
const cmd: Command = {
name: builder.name,
builder: builder,
ownerOnly: true,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
const mod = TTSManager.get.getModule('ElevenLabs') as
| ElevenLabsTTS
| undefined;
if (!mod) return;
const stability =
interaction.options.getNumber('stability') ||
ElevenLabsTTS.DEFAULT_SETTINGS.stability;
const similarityBoost =
interaction.options.getNumber('similarity-boost') ||
ElevenLabsTTS.DEFAULT_SETTINGS.similarity_boost;
const style =
interaction.options.getNumber('style') ||
ElevenLabsTTS.DEFAULT_SETTINGS.style;
const speed =
interaction.options.getNumber('speed') ||
ElevenLabsTTS.DEFAULT_SETTINGS.speed;
const speakerBoost =
interaction.options.getBoolean('speaker-boost') ||
ElevenLabsTTS.DEFAULT_SETTINGS.user_speaker_boost;
mod.setSettings({
stability: stability,
style: style,
speed: speed,
user_speaker_boost: speakerBoost,
similarity_boost: similarityBoost
});
const model = interaction.options.getString('model');
if (model) mod.setModel(model);
interaction.reply('ElevenLabs settings applied');
},
autocomplete: async (interaction: AutocompleteInteraction): Promise<void> => {
const focused = interaction.options.getFocused(true);
if (focused.name != 'model') return;
const mod = TTSManager.get.getModule('ElevenLabs') as
| ElevenLabsTTS
| undefined;
if (!mod) return;
const models = await mod.getModels();
const filtered: string[] = models
.filter((model) =>
model.toLowerCase().startsWith(focused.value.toLowerCase())
)
.slice(0, 25);
await interaction.respond(
filtered.map((choice) => ({ name: choice, value: choice }))
);
}
};
export default cmd;
+17 -2
View File
@@ -10,6 +10,7 @@ import { config } from '../../utils/config';
import { DatabaseManager } from '../../modules/db'; import { DatabaseManager } from '../../modules/db';
const URL_REGEX = /(?:https?|ftp):\/\/[\n\S]+/g; const URL_REGEX = /(?:https?|ftp):\/\/[\n\S]+/g;
const DISCORD_REGEX = /<(?::\w+:|@!*&*|#)[0-9]+>/g; // from: https://www.reddit.com/r/discordapp/comments/iibxms/if_anyone_needs_regex_to_match_an_emote_mention/
class TTSListener implements Command { class TTSListener implements Command {
private log: Logger; private log: Logger;
@@ -67,16 +68,30 @@ class TTSListener implements Command {
if (!voices) return; if (!voices) return;
if (!voices.includes(voiceName)) return; if (!voices.includes(voiceName)) return;
const msgFiltered = msg.content.replace(URL_REGEX, 'a link'); let msgFiltered = msg.content.replace(URL_REGEX, 'a link');
msgFiltered = msgFiltered.replace(DISCORD_REGEX, '');
if (msgFiltered.length === 0) return;
const audio = await ttsModule.generate(voiceName, msgFiltered); const audio = await ttsModule.generate(voiceName, msgFiltered);
if (!audio) {
this.log.error("TTS generation didn't return anything");
return;
}
if (audio?.data) { if (audio.data) {
const stream = const stream =
AudioStreamManager.get.getOrCreateStream(voiceConnection); AudioStreamManager.get.getOrCreateStream(voiceConnection);
const queue = stream.getQueue('TTS'); const queue = stream.getQueue('TTS');
queue.enqueue(Readable.from(audio.data)); queue.enqueue(Readable.from(audio.data));
} }
if (audio.error) {
this.log.error(
'Error occurred while generating message: (%s)',
audio.error
);
}
} catch (err) { } catch (err) {
this.log.error('Error occurred while processing TTS message (%s)', err); this.log.error('Error occurred while processing TTS message (%s)', err);
} }
+1 -1
View File
@@ -13,7 +13,7 @@ const builder = new SlashCommandBuilder()
const cmd: Command = { const cmd: Command = {
name: builder.name, name: builder.name,
builder: builder, builder: builder,
requiresAdmin: true,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => { execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral }); await interaction.deferReply({ flags: MessageFlags.Ephemeral });
+88 -46
View File
@@ -1,19 +1,24 @@
import { import {
AudioPlayer, AudioPlayer,
AudioPlayerStatus,
createAudioPlayer, createAudioPlayer,
createAudioResource, createAudioResource,
StreamType, StreamType,
VoiceConnection VoiceConnection,
VoiceConnectionStatus
} from '@discordjs/voice'; } from '@discordjs/voice';
import { AudioMixer } from 'node-audio-mixer'; import { AudioMixer } from 'node-audio-mixer';
import { PassThrough, Readable } from 'stream'; import { PassThrough, Readable } from 'stream';
import prism from 'prism-media'; import prism from 'prism-media';
const DURATION_EXTRA_MS = 1000;
export class StreamQueue { export class StreamQueue {
private queue: Readable[] = []; private queue: Readable[] = [];
private isPlaying = false; private isPlaying = false;
private mixer: MixedStream; private mixer: MixedStream;
private currentStop: (() => void) | null = null;
constructor(mixer: MixedStream) { constructor(mixer: MixedStream) {
this.mixer = mixer; this.mixer = mixer;
@@ -32,7 +37,10 @@ export class StreamQueue {
try { try {
if (nextStream) { if (nextStream) {
await this.mixer.playStream(nextStream); const { completion, stop } = this.mixer.playStream(nextStream);
this.currentStop = stop;
await completion;
this.currentStop = null;
} }
} catch (e) { } catch (e) {
console.error('Queue error:', e); console.error('Queue error:', e);
@@ -44,14 +52,21 @@ export class StreamQueue {
public clear() { public clear() {
this.queue = []; this.queue = [];
if (this.currentStop) {
this.currentStop();
this.currentStop = null;
}
}
public flush() {
this.mixer.flush();
} }
} }
export class MixedStream { export class MixedStream {
public readonly player: AudioPlayer; public readonly player: AudioPlayer;
private mixer: AudioMixer; private mixer: AudioMixer;
private output: PassThrough; private output: PassThrough | undefined;
private silenceInterval: NodeJS.Timeout;
private queues: Map<string, StreamQueue> = new Map(); private queues: Map<string, StreamQueue> = new Map();
@@ -63,33 +78,7 @@ export class MixedStream {
bitDepth: 16, bitDepth: 16,
sampleRate: 48000, sampleRate: 48000,
autoClose: false, autoClose: false,
generateSilence: false // does not work :< generateSilence: false
});
const silenceInput = this.mixer.createAudioInput({
channels: 2,
sampleRate: 48000,
bitDepth: 16,
volume: 100
});
const chunk = Buffer.alloc(3840);
this.silenceInterval = setInterval(() => {
if (silenceInput.writable && silenceInput.writableLength < 3840 * 10) {
silenceInput.write(chunk);
}
}, 20);
this.output = new PassThrough({ highWaterMark: 1024 * 16 });
this.mixer.pipe(this.output);
const resource = createAudioResource(this.output, {
inputType: StreamType.Raw
});
this.player.play(resource);
this.player.on('error', (error) => {
console.error('Error: ', error.message);
}); });
} }
@@ -102,8 +91,16 @@ export class MixedStream {
return queue; return queue;
} }
public playStream(source: Readable): Promise<void> { public playStream(source: Readable): {
return new Promise((resolve) => { completion: Promise<void>;
stop: () => void;
} {
let stopCallback: () => void = () => {};
const completion = new Promise<void>((resolve) => {
if (this.player.state.status === AudioPlayerStatus.Idle) {
this.setupPipeline();
}
const mixerInput = this.mixer.createAudioInput({ const mixerInput = this.mixer.createAudioInput({
channels: 2, channels: 2,
sampleRate: 48000, sampleRate: 48000,
@@ -131,32 +128,67 @@ export class MixedStream {
totalBytes += chunk.length; totalBytes += chunk.length;
}); });
let resolved = false;
const cleanup = () => {
if (resolved) return;
resolved = true;
source.unpipe(transcoder);
source.destroy();
transcoder.unpipe(mixerInput);
transcoder.destroy();
this.mixer.removeAudioinput(mixerInput);
mixerInput.destroy();
resolve();
};
stopCallback = cleanup;
transcoder.on('end', () => { transcoder.on('end', () => {
const durationMs = (totalBytes / 192000) * 1000; const durationMs = (totalBytes / 192000) * 1000 + DURATION_EXTRA_MS;
setTimeout(() => { setTimeout(() => {
source.unpipe(transcoder); cleanup();
transcoder.unpipe(mixerInput);
this.mixer.removeAudioinput(mixerInput);
transcoder.destroy();
resolve();
}, durationMs); }, durationMs);
}); });
transcoder.on('error', () => { transcoder.on('error', (err) => {
this.mixer.removeAudioinput(mixerInput); console.error('Transcoder error:', err);
resolve(); cleanup();
}); });
source.pipe(transcoder).pipe(mixerInput); source.pipe(transcoder).pipe(mixerInput);
}); });
return { completion, stop: stopCallback };
} }
public destroy(): void { public destroy(): void {
this.player.stop(); this.player.stop();
this.output.destroy(); if (this.output) this.output.destroy();
this.mixer.destroy(); this.mixer.destroy();
clearInterval(this.silenceInterval); }
public flush(): void {
this.player.stop();
this.setupPipeline();
}
private setupPipeline(): void {
if (this.output) {
this.mixer.unpipe(this.output);
this.output.destroy();
}
this.output = new PassThrough({ highWaterMark: 1024 * 256 });
this.mixer.pipe(this.output);
const resource = createAudioResource(this.output, {
inputType: StreamType.Raw
});
this.player.play(resource);
} }
} }
@@ -170,6 +202,16 @@ export class AudioStreamManager {
stream = new MixedStream(); stream = new MixedStream();
this.streams.set(conn, stream); this.streams.set(conn, stream);
conn.subscribe(stream.player); conn.subscribe(stream.player);
conn.on('stateChange', (_, newState) => {
if (
newState.status === VoiceConnectionStatus.Disconnected ||
newState.status === VoiceConnectionStatus.Destroyed
) {
this.destroyStream(conn);
}
});
return stream; return stream;
} }
@@ -182,8 +224,8 @@ export class AudioStreamManager {
} }
/* /*
singleton logic singleton logic
*/ */
static #instance: AudioStreamManager | null = null; static #instance: AudioStreamManager | null = null;
public static get get(): AudioStreamManager { public static get get(): AudioStreamManager {
+238 -18
View File
@@ -1,6 +1,39 @@
import { createHash, randomBytes } from 'crypto';
import { TTSModule, TTSResponse } from '../tts'; import { TTSModule, TTSResponse } from '../tts';
import { VoicesManager, Communicate } from 'edge-tts-universal'; import * as https from 'https';
import { WebSocket } from 'ws';
import { Logger } from '../../utils/log';
const CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
const AZURE_ENDPOINT = 'speech.platform.bing.com';
const READALOUD_PATH = `/consumer/speech/synthesize/readaloud`;
const WEBSOCKET_URL = `wss://${AZURE_ENDPOINT}${READALOUD_PATH}/edge/v1?TrustedClientToken=${CLIENT_TOKEN}`;
const VOICES_PATH = `${READALOUD_PATH}/voices/list?TrustedClientToken=${CLIENT_TOKEN}`;
const CHROME_VERSION = '138.0.7204.157';
const SEC_VERSION = `1-${CHROME_VERSION}`;
const USER_AGENT = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROME_VERSION.split('.')[0]}.0.0.0 Safari/537.36 Edg/${CHROME_VERSION.split('.')[0]}.0.0.0`;
const WIN_EPOCH = 11644473600;
const WS_RECONNECT_DELAY = 2000;
const MAX_RECONNECT_ATTEMPTS = 5;
interface PendingRequest {
resolve: (value: TTSResponse) => void;
reject: (reason: Error) => void;
audioBuff: Buffer[];
}
interface VoiceInfo {
// Name: string;
ShortName: string,
// Gender: string,
// Locale: string,
}
class AzureTTS implements TTSModule { class AzureTTS implements TTSModule {
private voices: Array<string> | undefined = undefined; private voices: Array<string> | undefined = undefined;
@@ -8,35 +41,222 @@ class AzureTTS implements TTSModule {
public name: string = 'Azure'; public name: string = 'Azure';
public defaultVoice: string = 'en-US-AvaNeural'; public defaultVoice: string = 'en-US-AvaNeural';
private ready: boolean = false;
private readyPromise: Promise<void> | null = null;
private readyResolve: (() => void) | null = null;
private ws: WebSocket | undefined = undefined;
private reconnectAttempts: number = 0;
private reconnectTimer: NodeJS.Timeout | null = null;
private isReconnecting: boolean = false;
private log: Logger;
// Map keyed by X-RequestId
private pendingRequests: Map<string, PendingRequest> = new Map();
constructor() {
this.log = new Logger('Azure TTS');
this.initializeConnection();
}
async getVoices(): Promise<Array<string> | undefined> { async getVoices(): Promise<Array<string> | undefined> {
if (!this.voices) { if (this.voices) return this.voices;
const voiceMgr = await VoicesManager.create();
const voiceQuery = await voiceMgr.find({});
this.voices = voiceQuery.map((voice) => voice.ShortName); const options: https.RequestOptions = {
} hostname: AZURE_ENDPOINT,
path: `${VOICES_PATH}&Sec-MS-GEC=${this.genSecToken()}&Sec-MS-GEC-Version=${SEC_VERSION}`,
method: 'GET',
headers: {
Pragma: 'no-cache',
'Cache-Control': 'no-cache',
'User-Agent': USER_AGENT,
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
Authority: 'speech.platform.bing.com',
'Sec-CH-UA': `" Not;A Brand";v="99", "Microsoft Edge";v="${CHROME_VERSION.split('.')[0]}", "Chromium";v="${CHROME_VERSION.split('.')[0]}"`,
'Sec-CH-UA-Mobile': '?0',
Accept: '*/*',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty'
}
};
return this.voices; return new Promise((resolve) => {
const req = https.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const body = Buffer.concat(chunks).toString();
this.voices = JSON.parse(body).map((v: VoiceInfo) => v.ShortName);
resolve(this.voices);
});
req.on('error', (err) => {
throw err;
});
res.on('aborted', () => {
throw new Error('Response aborted');
});
});
req.end();
});
} }
async generate(voice: string, text: string): Promise<TTSResponse> { async generate(voice: string, text: string): Promise<TTSResponse> {
const comm = new Communicate(text, { await this.readyPromise;
voice: voice if (!this.ready || !this.ws) return { error: 'Not initialized' };
const reqId = randomBytes(16).toString('hex');
const lang = voice.split('-').slice(0, 2).join('-');
return new Promise((resolve, reject) => {
this.pendingRequests.set(reqId, { resolve, reject, audioBuff: [] });
const headers = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nPath:ssml\r\n\r\n`;
const ssml = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="${lang}"><voice name="${voice}"><prosody rate="default" pitch="default">${this.escapeXml(text)}</prosody></voice></speak>`;
this.ws?.send(headers + ssml, (err) => {
if (err) {
this.pendingRequests.delete(reqId);
reject(err);
}
});
}); });
const buffers: Buffer[] = [];
for await (const chunk of comm.stream()) {
if (chunk.type === 'audio' && chunk.data) {
buffers.push(chunk.data);
}
}
return { data: Buffer.concat(buffers) };
} }
canBeUsed(): boolean { canBeUsed(): boolean {
return true; return true;
} }
private initializeConnection(): void {
this.ready = false;
this.readyPromise = new Promise((resolve) => {
this.readyResolve = resolve;
this.connect();
});
}
private connect(): void {
const url = `${WEBSOCKET_URL}&Sec-MS-GEC=${this.genSecToken()}&Sec-MS-GEC-Version=${SEC_VERSION}`;
this.ws = new WebSocket(url, {
host: 'speech.platform.bing.com',
origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
headers: {
Pragma: 'no-cache',
'User-Agent': USER_AGENT
}
});
this.ws.on('open', () => {
// this.log.verbose('WebSocket open');
this.reconnectAttempts = 0;
this.isReconnecting = false;
const config = `Content-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n
{
"context": {
"synthesis": {
"audio": {
"metadataoptions": { "sentenceBoundaryEnabled": "false", "wordBoundaryEnabled": "true" },
"outputFormat": "audio-24khz-48kbitrate-mono-mp3"
}
}
}
}`;
this.ws?.send(config.trim());
this.ready = true;
this.readyResolve?.();
});
this.ws.on('message', (data: Buffer, isBinary: boolean) => {
this.handleIncomingMessage(data, isBinary);
});
this.ws.on('close', (/*code, reason*/) => {
this.ready = false;
// this.log.verbose(`WS Closed: ${code}`);
this.rejectAllPending(new Error('Connection closed'));
this.scheduleReconnect();
});
this.ws.on('error', (err) => {
this.log.error('WS Error:', err);
});
}
private scheduleReconnect() {
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
const delay = WS_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts++);
setTimeout(() => this.connect(), delay);
}
private handleIncomingMessage(data: Buffer, isBinary: boolean) {
const message = data.toString();
const reqId = message.match(/X-RequestId:(.*?)\r\n/)?.[1];
if (!reqId) return;
const request = this.pendingRequests.get(reqId);
if (!request) return;
if (isBinary) {
const separator = 'Path:audio\r\n';
const index = data.indexOf(separator);
if (index !== -1) {
request.audioBuff.push(data.subarray(index + separator.length));
}
} else {
if (message.includes('Path:turn.end')) {
request.resolve({ data: Buffer.concat(request.audioBuff) });
this.pendingRequests.delete(reqId);
} else if (
message.includes('Path:turn.error') ||
message.includes('Path:error')
) {
request.reject(new Error('Azure synthesis error'));
this.pendingRequests.delete(reqId);
}
}
}
private rejectAllPending(err: Error) {
for (const [id, req] of this.pendingRequests) {
req.reject(err);
this.pendingRequests.delete(id);
}
}
private genSecToken(): string {
const ticks =
BigInt(Math.floor(Date.now() / 1000 + Number(WIN_EPOCH))) * 10000000n;
const roundedTicks = ticks - (ticks % 3000000000n);
const strToHash = `${roundedTicks}${CLIENT_TOKEN}`;
const hash = createHash('sha256');
hash.update(strToHash, 'ascii');
return hash.digest('hex').toUpperCase();
}
private escapeXml(unsafe: string): string {
return unsafe.replace(/[<>&"']/g, (c) => {
switch (c) {
case '<':
return '&lt;';
case '>':
return '&gt;';
case '&':
return '&amp;';
case '"':
return '&quot;';
case "'":
return '&apos;';
default:
return c;
}
});
}
} }
export default new AzureTTS(); export default new AzureTTS();
+276
View File
@@ -0,0 +1,276 @@
import { config } from '../../utils/config';
import { TTSModule, TTSResponse } from '../tts';
import * as https from 'https';
const ELEVENLABS_API_ENDPOINT = 'api.elevenlabs.io';
const FIREBASE_API_KEY = 'AIzaSyBSsRE_1Os04-bxpd5JTLIniy3UK4OqKys';
const FIREBASE_URL = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
/*
TO-DO: Implement previous text
*/
interface ElevenLabsVoice {
voice_id: string;
name: string;
// ...
}
interface ElevenLabsModel {
model_id: string;
name: string;
// ...
}
interface ElevenLabsVoicesRes {
voices?: Array<ElevenLabsVoice>;
}
interface ElevenLabsVoiceSettings {
stability: number;
similarity_boost: number;
style: number;
speed: number;
user_speaker_boost: boolean;
}
interface ElevenLabsStreamRequest {
text: string;
model_id: string;
voice_settings: ElevenLabsVoiceSettings;
}
interface FirebaseSession {
idToken: string;
refreshToken: string;
expiresAt: number;
}
export class ElevenLabsTTS implements TTSModule {
private voices: Array<ElevenLabsVoice> | undefined = undefined;
private models: Array<ElevenLabsModel> | undefined = undefined;
public name: string = 'ElevenLabs';
public settings: ElevenLabsVoiceSettings;
public modelId: string;
private session: FirebaseSession | undefined = undefined;
private initializationPromise: Promise<void> | undefined = undefined;
public static readonly DEFAULT_SETTINGS: ElevenLabsVoiceSettings = {
stability: 0.0,
similarity_boost: 0.5,
style: 1.0,
speed: 1.0,
user_speaker_boost: true
};
constructor() {
this.settings = ElevenLabsTTS.DEFAULT_SETTINGS;
this.modelId = 'eleven_v3';
if (this.canBeUsed()) this.initializationPromise = this.init();
this.setSettings = this.setSettings.bind(this);
this.setModel = this.setModel.bind(this);
this.getModels = this.getModels.bind(this);
}
private async init(): Promise<void> {
await this.ensureSession();
await Promise.all([this.fetchVoices(), this.fetchModels()]);
}
/*
TTSModule methods
*/
async getVoices(): Promise<Array<string> | undefined> {
if (this.voices) return this.voices.map((voice) => voice.name);
}
async generate(voice: string, text: string): Promise<TTSResponse> {
await this.initializationPromise;
await this.ensureSession();
if (!this.voices) return { error: 'no voices' };
if (!this.session) return { error: 'no session' };
const voiceData = this.voices.find((entry) => entry.name === voice);
if (!voiceData) return { error: 'Invalid voice' };
const options: https.RequestOptions = {
hostname: ELEVENLABS_API_ENDPOINT,
path: `/v1/text-to-speech/${voiceData.voice_id}/stream`,
method: 'POST',
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
origin: 'https://elevenlabs.io',
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
'Sec-Ch-Ua': '"Not)A;Brand";v="8", "Chromium";v="138"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Site': 'same-site',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
host: 'api.elevenlabs.io',
Authorization: `Bearer ${this.session.idToken}`
}
};
const body: ElevenLabsStreamRequest = {
text: text,
model_id: this.modelId,
voice_settings: this.settings
};
return new Promise((resolve) => {
const req = https.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
resolve({
data: Buffer.concat(chunks)
});
});
});
req.on('error', (error) => resolve({ error: error.message }));
req.write(JSON.stringify(body));
req.end();
});
}
canBeUsed(): boolean {
return config.tts_elevenlabs_refreshtoken != undefined;
}
/*
ElevenLabs specific methods
*/
public setSettings(settings: Partial<ElevenLabsVoiceSettings>) {
this.settings = { ...this.settings, ...settings };
}
public setModel(name: string) {
if (!this.models) return;
const model = this.models.find((mod) => mod.name == name);
if (!model) return;
this.modelId = model.model_id;
}
public getModels(): Array<string> {
if (!this.models) return [];
return this.models.map((mod) => mod.name);
}
private async fetchVoices(): Promise<void> {
if (!this.session) return;
const opt: https.RequestOptions = {
hostname: ELEVENLABS_API_ENDPOINT,
path: '/v2/voices',
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${this.session.idToken}`,
'Content-Type': 'application/json'
}
};
return new Promise((resolve) => {
const req = https.get(opt, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const voicesJSON = Buffer.concat(chunks).toString('utf-8');
const voicesParsed = JSON.parse(voicesJSON) as ElevenLabsVoicesRes;
if (!voicesParsed.voices) {
console.error('ElevenLabs voice fetch responded:', voicesJSON);
throw new Error('Failed to get ElevenLabs voices');
}
this.voices = voicesParsed.voices;
resolve();
});
});
req.on('error', (err) => {
console.error('Failed to get ElevenLabs voices:', err);
throw err;
});
});
}
private async fetchModels(): Promise<void> {
if (!this.session) return;
const opt: https.RequestOptions = {
hostname: ELEVENLABS_API_ENDPOINT,
path: '/v1/models',
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${this.session.idToken}`,
'Content-Type': 'application/json'
}
};
return new Promise((resolve) => {
const req = https.get(opt, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const modelsJSON = Buffer.concat(chunks).toString('utf-8');
const modelsParsed = JSON.parse(modelsJSON) as Array<ElevenLabsModel>;
this.models = modelsParsed;
resolve();
});
});
req.on('error', (err) => {
console.error('Failed to get ElevenLabs models:', err);
throw err;
});
});
}
private async ensureSession(): Promise<void> {
if (this.session && Date.now() < this.session.expiresAt - 300000) return;
const refreshToken =
this.session?.refreshToken || config.tts_elevenlabs_refreshtoken;
if (!refreshToken) throw new Error('No refresh token available');
const response = await fetch(FIREBASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: 'https://elevenlabs.io/',
Origin: 'https://elevenlabs.io'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken
})
});
if (!response.ok)
throw new Error(`Auth Refresh Failed: ${await response.text()}`);
const data = await response.json();
this.session = {
idToken: data.id_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + parseInt(data.expires_in) * 1000
};
}
}
export default new ElevenLabsTTS();
+115
View File
@@ -0,0 +1,115 @@
import { config } from '../../utils/config';
import { TTSModule, TTSResponse } from '../tts';
import * as https from 'https';
import * as zlib from 'zlib';
import TIKTOK_TTS_VOICES from './tiktok_voices.json';
const TIKTOK_API_ENDPOINT = 'api16-normal-v6.tiktokv.com';
class TikTokTTS implements TTSModule {
public name: string = 'TikTok';
public defaultVoice: string = 'en_us_001';
async getVoices(): Promise<Array<string> | undefined> {
return TIKTOK_TTS_VOICES.voices;
}
async generate(voice: string, text: string): Promise<TTSResponse> {
const reqText = encodeURIComponent(text);
const path = `/media/api/text/speech/invoke/?text_speaker=${voice}&req_text=${reqText}&speaker_map_type=0&aid=1233`;
const options: https.RequestOptions = {
hostname: TIKTOK_API_ENDPOINT,
path: path,
method: 'POST',
headers: {
'User-Agent':
'com.zhiliaoapp.musically/2022600030 (Linux; U; Android 7.1.2; es_ES; SM-G988N; Build/NRD90M;tt-ok/3.12.13.1)',
Cookie: `sessionid=${config.tts_tiktok_sessionid}`,
'Accept-Encoding': 'gzip,deflate,compress',
'Content-Type': 'application/x-www-form-urlencoded'
}
};
return new Promise((resolve) => {
const req = https.request(options, (res) => {
const chunks: Buffer[] = [];
const encoding = res.headers['content-encoding'];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
try {
const buffer = Buffer.concat(chunks);
const decompressBuffer = (buf: Buffer): Promise<Buffer> => {
return new Promise((decompressResolve, decompressReject) => {
if (encoding === 'gzip' || encoding === 'deflate') {
zlib.unzip(buf, (err: Error | null, decompressed: Buffer) => {
if (err) decompressReject(err);
else decompressResolve(decompressed);
});
} else {
decompressResolve(buf);
}
});
};
decompressBuffer(buffer)
.then((decompressed) => {
const result = JSON.parse(decompressed.toString());
const statusCode = result?.status_code;
if (statusCode !== 0) {
const errorMsg = this.handleStatusError(statusCode);
return resolve({ error: errorMsg });
}
const voiceStr = result?.data?.v_str;
if (!voiceStr) {
return resolve({ error: 'No audio data received' });
}
resolve({ data: Buffer.from(voiceStr, 'base64') });
})
.catch((err) => {
resolve({ error: `Decompression/Parse error: ${err.message}` });
});
} catch (err) {
resolve({ error: `Parse error: ${err}` });
}
});
});
req.on('error', (err) => resolve({ error: err.message }));
req.on('timeout', () => {
req.destroy();
resolve({ error: 'timed out' });
});
req.write('');
req.end();
});
}
canBeUsed(): boolean {
return config.tts_tiktok_sessionid != undefined;
}
handleStatusError(code: number): string {
switch (code) {
case 1:
return 'Session ID may be invalid or expired';
case 2:
return 'Text is too long';
case 4:
return 'Invalid voice';
case 5:
return 'No session id.';
}
return `Unknown error code: ${code}`;
}
}
export default new TikTokTTS();
+99
View File
@@ -0,0 +1,99 @@
{
"voices": [
"en_us_ghostface",
"en_us_chewbacca",
"en_us_c3po",
"en_us_stitch",
"en_us_stormtrooper",
"en_us_rocket",
"en_female_madam_leota",
"en_male_ghosthost",
"en_male_pirate",
"en_au_001",
"en_au_002",
"en_uk_001",
"en_uk_003",
"en_us_001",
"en_us_002",
"en_us_006",
"en_us_007",
"en_us_009",
"en_us_010",
"en_male_jomboy",
"en_male_cody",
"en_female_samc",
"en_female_makeup",
"en_female_richgirl",
"en_male_grinch",
"en_male_deadpool",
"en_male_jarvis",
"en_male_ashmagic",
"en_male_olantekkers",
"en_male_ukneighbor",
"en_male_ukbutler",
"en_female_shenna",
"en_female_pansino",
"en_male_trevor",
"en_female_betty",
"en_male_cupid",
"en_female_grandma",
"en_male_m2_xhxs_m03_christmas",
"en_male_santa_narration",
"en_male_sing_deep_jingle",
"en_male_santa_effect",
"en_female_ht_f08_newyear",
"en_male_wizard",
"en_female_ht_f08_halloween",
"fr_001",
"fr_002",
"de_001",
"de_002",
"es_002",
"es_mx_002",
"br_001",
"br_003",
"br_004",
"br_005",
"bp_female_ivete",
"bp_female_ludmilla",
"pt_female_lhays",
"pt_female_laizza",
"pt_male_bueno",
"id_001",
"jp_001",
"jp_003",
"jp_005",
"jp_006",
"kr_002",
"kr_003",
"kr_004",
"jp_female_fujicochan",
"jp_female_hasegawariona",
"jp_male_keiichinakano",
"jp_female_oomaeaika",
"jp_male_yujinchigusa",
"jp_female_shirou",
"jp_male_tamawakazuki",
"jp_female_kaorishoji",
"jp_female_yagishaki",
"jp_male_hikakin",
"jp_female_rei",
"jp_male_shuichiro",
"jp_male_matsudake",
"jp_female_machikoriiita",
"jp_male_matsuo",
"jp_male_osada",
"en_female_f08_salut_damour",
"en_male_m03_lobby",
"en_female_f08_warmy_breeze",
"en_male_m03_sunshine_soon",
"en_female_ht_f08_glorious",
"en_male_sing_funny_it_goes_up",
"en_male_m2_xhxs_m03_silly",
"en_female_ht_f08_wonderful_world",
"en_male_sing_funny_thanksgiving",
"en_male_narration",
"en_male_funny",
"en_female_emotional"
]
}
+8 -4
View File
@@ -49,13 +49,17 @@ export class TTSManager {
if (!isModule(filePath)) return; if (!isModule(filePath)) return;
const modRaw = await import(`file://${filePath}`); const modRaw = await import(`file://${filePath}`);
if (!modRaw) {
if (!modRaw || !modRaw.default) { this.log.warning('Mod import failed for %s', filePath);
this.log.warning('Invalid module format in %s', filePath);
return; return;
} }
const mod = modRaw.default as TTSModule; const mod = modRaw.default?.default || modRaw.default || modRaw;
if (!mod.name || typeof mod.generate !== 'function') {
this.log.warning('Invalid module format in %s', filePath);
return;
}
this.log.verbose(`Loaded TTS mode: ${mod.name}`); this.log.verbose(`Loaded TTS mode: ${mod.name}`);
this.modules.push(mod); this.modules.push(mod);
+5 -5
View File
@@ -6,8 +6,8 @@ export interface Config {
tts_default_mode: string | undefined; tts_default_mode: string | undefined;
tts_default_voice: string | undefined; tts_default_voice: string | undefined;
tts_azure_key: string | undefined; tts_elevenlabs_refreshtoken: string | undefined;
tts_elevenlabs_key: string | undefined; tts_tiktok_sessionid: string | undefined;
steam_webapi_key: string | undefined; steam_webapi_key: string | undefined;
@@ -29,11 +29,11 @@ function loadConfig(): Config {
owner_id: process.env.DISCORD_OWNER_ID, owner_id: process.env.DISCORD_OWNER_ID,
tts_default_mode: process.env.DEFAULT_TTS_MODE, tts_default_mode: process.env.DEFAULT_TTS_MODE,
tts_default_voice: process.env.DEFAULT_TTS_VOICE, tts_default_voice: process.env.DEFAULT_TTS_VOICE,
tts_azure_key: process.env.TTS_AZURE_KEY, tts_elevenlabs_refreshtoken: process.env.TTS_ELEVENLABS_REFRESHTOKEN,
tts_elevenlabs_key: process.env.TTS_ELEVENLABS_KEY,
steam_webapi_key: process.env.STEAM_WEBAPI_KEY, steam_webapi_key: process.env.STEAM_WEBAPI_KEY,
aws_access_id: process.env.AWS_ACCESS_ID, aws_access_id: process.env.AWS_ACCESS_ID,
aws_access_key: process.env.AWS_ACCESS_KEY aws_access_key: process.env.AWS_ACCESS_KEY,
tts_tiktok_sessionid: process.env.TTS_TIKTOK_SESSIONID
}; };
} }