diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..d044bbe9c80b67b52334a2d4a45292e30515faf6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +models +sandbox +audio.pipe +video.pipe \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..479c48d2e351566836978b44383c5c38d7f25f25 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ + +# the secret micro service key used in various API spaces +MICROSERVICE_API_SECRET_TOKEN="" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ceb531b36f1dd61e30fa368bb33fee0f2a9d2a70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +samples +node_modules +*.log +*.bin +.DS_Store +.venv +*.mp4 +*.wav +*.mp3 +*.webp +sandbox +scripts +.env.local diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000000000000000000000000000000..016efd8a0159356fab78d3c81694d34795cdf29c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.10.0 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e81170c9a57eea7aebd31b778c5f945a2ec00d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# And Node 20 +FROM node:20-alpine + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apk update + +RUN apk add alpine-sdk pkgconfig + +# For FFMPEG and gl concat +RUN apk add curl python3 python3-dev libx11-dev libsm-dev libxrender libxext-dev mesa-dev xvfb libxi-dev glew-dev + +# For Puppeteer +RUN apk add build-base gcompat udev ttf-opensans chromium + +RUN apk add ffmpeg + +# Set up a new user named "user" with user ID 1000 +RUN adduser --disabled-password --uid 1001 user + +# Switch to the "user" user +USER user + +# Set home to the user's home directory +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH + +# Set the working directory to the user's home directory +WORKDIR $HOME/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY --chown=user package*.json $HOME/app + +# make sure the .env is copied as well +COPY --chown=user .env $HOME/app + +RUN ffmpeg -version + +RUN npm install + +# Copy the current directory contents into the container at $HOME/app setting the owner to the user +COPY --chown=user . $HOME/app + +EXPOSE 7860 + +# we can't use this (it time out) +# CMD [ "xvfb-run", "-s", "-ac -screen 0 1920x1080x24", "npm", "run", "start" ] +CMD [ "npm", "run", "start" ] \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8a168df3de2a29b92aaa94439118cfa3d3170ec4 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +--- +title: AI Tube Clap Exporter +emoji: πŸΏπŸ€– +colorFrom: red +colorTo: blue +sdk: docker +pinned: false +app_port: 7860 +--- + +Export a full .clap (with all its assets already in) to a video + +# Installation + +It is important that you make sure to use the correct version of Node (Node 20) + +1. `nvm use` +2. `npm i` +3. clone `.env` to `.env.local` +4. edit `.env.local` to define the secrets / api access keys +5. `npm run start` + +# Testing the Docker image + +Note: you need to install Docker, and it needs to be already running. + +You will also need to build it for *your* architecture. + +```bash +docker build --platform linux/arm64 -t ai-tube-clap-exporter . +docker run -it -p 7860:7860 ai-tube-clap-exporter +``` + +# Architecture + +AI Channels are just Hugging Face datasets. + +For now, we keep everything into one big JSON index, but don't worry we can migrate this to something more efficient, such as Redis (eg. using Upstash for convenience). \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..feb7c7b2acb1c22e4f7e72c551951c77b7de59c0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3492 @@ +{ + "name": "ai-tube-clap-exporter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-tube-clap-exporter", + "version": "1.0.0", + "license": "Apache License", + "dependencies": { + "@types/express": "^4.17.17", + "@types/fluent-ffmpeg": "^2.1.24", + "@types/uuid": "^9.0.2", + "dotenv": "^16.3.1", + "eventsource-parser": "^1.0.0", + "express": "^4.18.2", + "fluent-ffmpeg": "^2.1.2", + "fs-extra": "^11.1.1", + "mime-types": "^2.1.35", + "node-fetch": "^3.3.1", + "puppeteer": "^22.7.0", + "sharp": "^0.33.3", + "temp-dir": "^3.0.0", + "ts-node": "^10.9.1", + "type-fest": "^4.8.2", + "uuid": "^9.0.0", + "yaml": "^2.4.1" + }, + "devDependencies": { + "@types/mime-types": "^2.1.4", + "@types/node": "^20.12.7", + "tsx": "^4.7.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", + "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", + "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", + "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", + "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", + "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", + "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", + "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", + "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", + "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", + "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.1.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", + "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", + "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.2.tgz", + "integrity": "sha512-hZ/JhxPIceWaGSEzUZp83/8M49CoxlkuThfTR7t4AoCu5+ZvJ3vktLm60Otww2TXeROB5igiZ8D9oPQh6ckBVg==", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", + "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", + "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^1.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.1.tgz", + "integrity": "sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", + "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "optional": true, + "dependencies": { + "streamx": "^2.16.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chromium-bidi": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.17.tgz", + "integrity": "sha512-BqOuIWUgTPj8ayuBFJUYCCuwIcwjBsb3/614P7tt1bEPJ4i1M0kCdIl0Wi9xhtswBXnfO2bTpTMkHD71H8rJMg==", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.22.4" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1273771", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1273771.tgz", + "integrity": "sha512-QDbb27xcTVReQQW/GHJsdQqGKwYBE7re7gxehj467kKP2DKuYBUj6i2k5LRiAC66J1yZG/9gsxooz/s9pcm0Og==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "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/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "22.7.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.7.0.tgz", + "integrity": "sha512-s1ulKFZKW3lwWCtNu0VrRLfRaanFHxIv7F8UFYpLo8dPTZcI4wB4EAOD0sKT3SKP+1Rsjodb9WFsK78yKQ6i9Q==", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "2.2.2", + "cosmiconfig": "9.0.0", + "devtools-protocol": "0.0.1273771", + "puppeteer-core": "22.7.0" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "22.7.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.7.0.tgz", + "integrity": "sha512-9Q+L3VD7cfhXnxv6AqwTHsVb/+/uXENByhPrgvwQ49wvzQwtf1d6b7v6gpoG3tpRdwYjxoV1eHTD8tFahww09g==", + "dependencies": { + "@puppeteer/browsers": "2.2.2", + "chromium-bidi": "0.5.17", + "debug": "4.3.4", + "devtools-protocol": "0.0.1273771", + "ws": "8.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/sharp": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", + "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.0" + }, + "engines": { + "libvips": ">=8.15.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.3", + "@img/sharp-darwin-x64": "0.33.3", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.3", + "@img/sharp-linux-arm64": "0.33.3", + "@img/sharp-linux-s390x": "0.33.3", + "@img/sharp-linux-x64": "0.33.3", + "@img/sharp-linuxmusl-arm64": "0.33.3", + "@img/sharp-linuxmusl-x64": "0.33.3", + "@img/sharp-wasm32": "0.33.3", + "@img/sharp-win32-ia32": "0.33.3", + "@img/sharp-win32-x64": "0.33.3" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsx": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.2.tgz", + "integrity": "sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==", + "dev": true, + "dependencies": { + "esbuild": "~0.19.10", + "get-tsconfig": "^4.7.2" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.17.0.tgz", + "integrity": "sha512-9flrz1zkfLRH3jO3bLflmTxryzKMxVa7841VeMgBaNQGY6vH4RCcpN/sQLB7mQQYh1GZ5utT2deypMuCy4yicw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..633ccd13fb56fdfac23da7a02e015f301c6318cc --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "ai-tube-clap-exporter", + "version": "1.0.0", + "description": "A service to convert a .clap (will all its assets) to a video file", + "main": "src/index.mts", + "scripts": { + "start": "tsx src/index.mts", + "dev": "tsx src/index.mts", + "docker": "npm run docker:build && npm run docker:run", + "docker:build": "docker build -t ai-tube-robot .", + "docker:run": "docker run -it -p 7860:7860 ai-tube-robot", + "alchemy:test": "tsx src/core/alchemy/test.mts" + }, + "author": "Julian Bilcke ", + "license": "Apache License", + "dependencies": { + "@types/express": "^4.17.17", + "@types/fluent-ffmpeg": "^2.1.24", + "@types/uuid": "^9.0.2", + "dotenv": "^16.3.1", + "eventsource-parser": "^1.0.0", + "express": "^4.18.2", + "fluent-ffmpeg": "^2.1.2", + "fs-extra": "^11.1.1", + "mime-types": "^2.1.35", + "node-fetch": "^3.3.1", + "puppeteer": "^22.7.0", + "sharp": "^0.33.3", + "temp-dir": "^3.0.0", + "ts-node": "^10.9.1", + "type-fest": "^4.8.2", + "uuid": "^9.0.0", + "yaml": "^2.4.1" + }, + "devDependencies": { + "@types/mime-types": "^2.1.4", + "@types/node": "^20.12.7", + "tsx": "^4.7.0" + } +} diff --git a/src/core/base64/addBase64.mts b/src/core/base64/addBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..5c724a3d069d7d2f02a1907372df47e02f0c63fc --- /dev/null +++ b/src/core/base64/addBase64.mts @@ -0,0 +1,51 @@ +export function addBase64Header( + image?: string, + format?: + | "jpeg" | "jpg" | "png" | "webp" | "heic" + | "mp3" | "wav" + | "mp4" | "webm" + | string +) { + + if (!image || typeof image !== "string" || image.length < 60) { + return "" + } + + const ext = (`${format || ""}`.split(".").pop() || "").toLowerCase().trim() + + let mime = "" + if ( + ext === "jpeg" || + ext === "jpg") { + mime = "image/jpeg" + } else if ( + ext === "webp" + ) { + mime = "image/webp" + } else if ( + ext === "png") { + mime = "image/png" + } else if (ext === "heic") { + mime = "image/heic" + } else if (ext === "mp3") { + mime = "audio/mp3" + } else if (ext === "mp4") { + mime = "video/mp4" + } else if (ext === "webm") { + mime = "video/webm" + } else if (ext === "wav") { + mime = "audio/wav" + } else { + throw new Error(`addBase64Header failed (unsupported format: ${format})`) + } + + if (image.startsWith('data:')) { + if (image.startsWith(`data:${mime};base64,`)) { + return image + } else { + throw new Error(`addBase64Header failed (input string is NOT a ${mime} image)`) + } + } else { + return `data:${mime};base64,${image}` + } +} \ No newline at end of file diff --git a/src/core/base64/dataUriToBlob.mts b/src/core/base64/dataUriToBlob.mts new file mode 100644 index 0000000000000000000000000000000000000000..f60f4250d3ed35f9968a2f1fb18fa0d50940915f --- /dev/null +++ b/src/core/base64/dataUriToBlob.mts @@ -0,0 +1,15 @@ + +export function dataUriToBlob(dataURI = "", defaultContentType = ""): Blob { + dataURI = dataURI.replace(/^data:/, ''); + + const type = dataURI.match(/(?:image|application|video|audio|text)\/[^;]+/)?.[0] || defaultContentType; + const base64 = dataURI.replace(/^[^,]+,/, ''); + const arrayBuffer = new ArrayBuffer(base64.length); + const typedArray = new Uint8Array(arrayBuffer); + + for (let i = 0; i < base64.length; i++) { + typedArray[i] = base64.charCodeAt(i); + } + + return new Blob([arrayBuffer], { type }); +} \ No newline at end of file diff --git a/src/core/base64/extractBase64.mts b/src/core/base64/extractBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..59b01ad27f0d80ff2e3766fda283e76fc9a08b04 --- /dev/null +++ b/src/core/base64/extractBase64.mts @@ -0,0 +1,36 @@ +/** + * break a base64 string into sub-components + */ +export function extractBase64(base64: string = ""): { + mimetype: string; + extension: string; + data: string; + buffer: Buffer; + blob: Blob; +} { + // console.log(`extractBase64(${base64.slice(0, 120)})`) + // Regular expression to extract the MIME type and the base64 data + const matches = base64.match(/^data:([A-Za-z-+/]+);base64,(.+)$/) + + // console.log("matches:", matches) + + if (!matches || matches.length !== 3) { + throw new Error("Invalid base64 string") + } + + const mimetype = matches[1] || "" + const data = matches[2] || "" + const buffer = Buffer.from(data, "base64") + const blob = new Blob([buffer]) + + // this should be enough for most media formats (jpeg, png, webp, mp4) + const extension = mimetype.split("/").pop() || "" + + return { + mimetype, + extension, + data, + buffer, + blob, + } +} \ No newline at end of file diff --git a/src/core/clap/getClapAssetSourceType.mts b/src/core/clap/getClapAssetSourceType.mts new file mode 100644 index 0000000000000000000000000000000000000000..0a5f3e86de1b3b87b6ca70cb0d38df7f61a2e244 --- /dev/null +++ b/src/core/clap/getClapAssetSourceType.mts @@ -0,0 +1,25 @@ +import { ClapAssetSource } from "./types.mts" + +export function getClapAssetSourceType(input: string = ""): ClapAssetSource { + + const str = `${input || ""}`.trim() + + if (!str || !str.length) { + return "EMPTY" + } + + if (str.startsWith("https://") || str.startsWith("http://")) { + return "REMOTE" + } + + // note that "path" assets are potentially a security risk, they need to be treated with care + if (str.startsWith("/") || str.startsWith("../") || str.startsWith("./")) { + return "PATH" + } + + if (str.startsWith("data:")) { + return "DATA" + } + + return "PROMPT" +} \ No newline at end of file diff --git a/src/core/clap/parseClap.mts b/src/core/clap/parseClap.mts new file mode 100644 index 0000000000000000000000000000000000000000..d1dac29b68690ea20b7ebecfff80a93766852eec --- /dev/null +++ b/src/core/clap/parseClap.mts @@ -0,0 +1,320 @@ + +import { v4 as uuidv4 } from "uuid" +import YAML from "yaml" + +import { ClapHeader, ClapMeta, ClapModel, ClapProject, ClapScene, ClapSegment } from "./types.mts" +import { getValidNumber } from "../parsers/getValidNumber.mts" +import { dataUriToBlob } from "../base64/dataUriToBlob.mts" + +type StringOrBlob = string | Blob + +/** + * Import a clap file from various data sources into an ClapProject + * + * Inputs can be: + * - a Clap project (which is an object) + * - an URL to a remote .clap file + * - a string containing a YAML array + * - a data uri containing a gzipped YAML array + * - a Blob containing a gzipped YAML array + * + * note: it is not really async, because for some reason YAML.parse is a blocking call like for JSON, + * there is no async version although we are now in the 20s not 90s + */ +export async function parseClap(src?: ClapProject | string | Blob, debug = false): Promise { + + try { + if ( + typeof src === "object" && + Array.isArray( (src as any)?.scenes) && + Array.isArray((src as any)?.models) + ) { + if (debug) { + console.log("parseClap: input is already a Clap file, nothing to do:", src) + } + // we can skip verification + return src as ClapProject + } + } catch (err) { + // well, this is not a clap project + } + + let stringOrBlob = (src || "") as StringOrBlob + + // both should work + const dataUriHeader1 = "data:application/x-gzip;base64," + const dataUriHeader2 = "data:application/octet-stream;base64," + + const inputIsString = typeof stringOrBlob === "string" + const inputIsDataUri = typeof stringOrBlob === "string" ? stringOrBlob.startsWith(dataUriHeader1) || stringOrBlob.startsWith(dataUriHeader2) : false + const inputIsRemoteFile = typeof stringOrBlob === "string" ? (stringOrBlob.startsWith("http://") || stringOrBlob.startsWith("https://")) : false + + let inputIsBlob = typeof stringOrBlob !== "string" + + let inputYamlArrayString = "" + + if (debug) { + console.log(`parseClap: pre-analysis: ${JSON.stringify({ + inputIsString, + inputIsBlob, + inputIsDataUri, + inputIsRemoteFile + }, null, 2)}`) + } + + if (typeof stringOrBlob === "string") { + if (debug) { + console.log("parseClap: input is a string ", stringOrBlob.slice(0, 120)) + } + if (inputIsDataUri) { + if (debug) { + console.log(`parseClap: input is a data uri archive`) + } + stringOrBlob = dataUriToBlob(stringOrBlob, "application/x-gzip") + if (debug) { + console.log(`parseClap: inputBlob = `, stringOrBlob) + } + inputIsBlob = true + } else if (inputIsRemoteFile) { + try { + if (debug) { + console.log(`parseClap: input is a remote .clap file`) + } + const res = await fetch(stringOrBlob) + stringOrBlob = await res.blob() + if (!stringOrBlob) { throw new Error("blob is empty") } + inputIsBlob = true + } catch (err) { + // url seems invalid + throw new Error(`failed to download the .clap file (${err})`) + } + } else { + if (debug) { + console.log("parseClap: input is a text string containing a YAML array") + } + inputYamlArrayString = stringOrBlob + inputIsBlob = false + } + } + + if (typeof stringOrBlob !== "string" && stringOrBlob) { + if (debug) { + console.log("parseClap: decompressing the blob..") + } + // Decompress the input blob using gzip + const decompressedStream = stringOrBlob.stream().pipeThrough(new DecompressionStream('gzip')) + + try { + // Convert the stream to text using a Response object + const decompressedOutput = new Response(decompressedStream) + // decompressedOutput.headers.set("Content-Type", "application/x-gzip") + if (debug) { + console.log("parseClap: decompressedOutput: ", decompressedOutput) + } + // const blobAgain = await decompressedOutput.blob() + inputYamlArrayString = await decompressedOutput.text() + + if (debug && inputYamlArrayString) { + console.log("parseClap: successfully decompressed the blob!") + } + } catch (err) { + const message = `parseClap: failed to decompress (${err})` + console.error(message) + throw new Error(message) + } + } + + // we don't need this anymore I think + // new Blob([inputStringOrBlob], { type: "application/x-yaml" }) + + let maybeArray: any = {} + try { + if (debug) { + console.log("parseClap: parsing the YAML array..") + } + // Parse YAML string to raw data + maybeArray = YAML.parse(inputYamlArrayString) + } catch (err) { + throw new Error("invalid clap file (input string is not YAML)") + } + + if (!Array.isArray(maybeArray) || maybeArray.length < 2) { + throw new Error("invalid clap file (need a clap format header block and project metadata block)") + } + + if (debug) { + console.log("parseClap: the YAML seems okay, continuing decoding..") + } + + const maybeClapHeader = maybeArray[0] as ClapHeader + + if (maybeClapHeader.format !== "clap-0") { + throw new Error("invalid clap file (sorry, but you can't make up version numbers like that)") + } + + + const maybeClapMeta = maybeArray[1] as ClapMeta + + const clapMeta: ClapMeta = { + id: typeof maybeClapMeta.title === "string" ? maybeClapMeta.id : uuidv4(), + title: typeof maybeClapMeta.title === "string" ? maybeClapMeta.title : "", + description: typeof maybeClapMeta.description === "string" ? maybeClapMeta.description : "", + synopsis: typeof maybeClapMeta.synopsis === "string" ? maybeClapMeta.synopsis : "", + licence: typeof maybeClapMeta.licence === "string" ? maybeClapMeta.licence : "", + orientation: maybeClapMeta.orientation === "portrait" ? "portrait" : maybeClapMeta.orientation === "square" ? "square" : "landscape", + durationInMs: getValidNumber(maybeClapMeta.durationInMs, 1000, Number.MAX_SAFE_INTEGER, 4000), + width: getValidNumber(maybeClapMeta.width, 128, 8192, 1024), + height: getValidNumber(maybeClapMeta.height, 128, 8192, 576), + defaultVideoModel: typeof maybeClapMeta.defaultVideoModel === "string" ? maybeClapMeta.defaultVideoModel : "SVD", + extraPositivePrompt: Array.isArray(maybeClapMeta.extraPositivePrompt) ? maybeClapMeta.extraPositivePrompt : [], + screenplay: typeof maybeClapMeta.screenplay === "string" ? maybeClapMeta.screenplay : "", + isLoop: typeof maybeClapMeta.isLoop === "boolean" ? maybeClapMeta.isLoop : false, + isInteractive: typeof maybeClapMeta.isInteractive === "boolean" ? maybeClapMeta.isInteractive : false, + } + + /* + in case we want to support streaming (mix of models and segments etc), we could do it this way: + + const maybeModelsOrSegments = rawData.slice(2) + maybeModelsOrSegments.forEach((unknownElement: any) => { + if (isValidNumber(unknownElement?.track)) { + maybeSegments.push(unknownElement as ClapSegment) + } else { + maybeModels.push(unknownElement as ClapModel) + } + }) + */ + + + const expectedNumberOfModels = maybeClapHeader.numberOfModels || 0 + const expectedNumberOfScenes = maybeClapHeader.numberOfScenes || 0 + const expectedNumberOfSegments = maybeClapHeader.numberOfSegments || 0 + + // note: we assume the order is strictly enforced! + // if you implement streaming (mix of models and segments) you will have to rewrite this! + + const afterTheHeaders = 2 + const afterTheModels = afterTheHeaders + expectedNumberOfModels + + const afterTheScenes = afterTheModels + expectedNumberOfScenes + + // note: if there are no expected models, maybeModels will be empty + const maybeModels = maybeArray.slice(afterTheHeaders, afterTheModels) as ClapModel[] + + // note: if there are no expected scenes, maybeScenes will be empty + const maybeScenes = maybeArray.slice(afterTheModels, afterTheScenes) as ClapScene[] + + const maybeSegments = maybeArray.slice(afterTheScenes) as ClapSegment[] + + const clapModels: ClapModel[] = maybeModels.map(({ + id, + category, + triggerName, + label, + description, + author, + thumbnailUrl, + seed, + assetSourceType, + assetUrl, + age, + gender, + region, + appearance, + voiceVendor, + voiceId, + }) => ({ + // TODO: we should verify each of those, probably + id, + category, + triggerName, + label, + description, + author, + thumbnailUrl, + seed, + assetSourceType, + assetUrl, + age, + gender, + region, + appearance, + voiceVendor, + voiceId, + })) + + const clapScenes: ClapScene[] = maybeScenes.map(({ + id, + scene, + line, + rawLine, + sequenceFullText, + sequenceStartAtLine, + sequenceEndAtLine, + startAtLine, + endAtLine, + events, + }) => ({ + id, + scene, + line, + rawLine, + sequenceFullText, + sequenceStartAtLine, + sequenceEndAtLine, + startAtLine, + endAtLine, + events: events.map(e => e) + })) + + const clapSegments: ClapSegment[] = maybeSegments.map(({ + id, + track, + startTimeInMs, + endTimeInMs, + category, + modelId, + sceneId, + prompt, + label, + outputType, + renderId, + status, + assetUrl, + assetDurationInMs, + createdBy, + editedBy, + outputGain, + seed, + }) => ({ + // TODO: we should verify each of those, probably + id, + track, + startTimeInMs, + endTimeInMs, + category, + modelId, + sceneId, + prompt, + label, + outputType, + renderId, + status, + assetUrl, + assetDurationInMs, + createdBy, + editedBy, + outputGain, + seed, + })) + + if (debug) { + console.log(`parseClap: successfully parsed ${clapModels.length} models, ${clapScenes.length} scenes and ${clapSegments.length} segments`) + } + return { + meta: clapMeta, + models: clapModels, + scenes: clapScenes, + segments: clapSegments + } +} diff --git a/src/core/clap/types.mts b/src/core/clap/types.mts new file mode 100644 index 0000000000000000000000000000000000000000..a15acdfc6607f34df1ee62132b4fbd8ad247fe3b --- /dev/null +++ b/src/core/clap/types.mts @@ -0,0 +1,203 @@ + +export type ClapSegmentCategory = + | "splat" + | "mesh" + | "depth" + | "event" + | "interface" + | "phenomenon" + | "video" + | "storyboard" + | "transition" + | "characters" + | "location" + | "time" + | "era" + | "lighting" + | "weather" + | "action" + | "music" + | "sound" + | "dialogue" + | "style" + | "camera" + | "generic" + +export type ClapOutputType = + | "text" + | "animation" + | "interface" + | "event" + | "phenomenon" + | "transition" + | "image" + | "video" + | "audio" + +export type ClapSegmentStatus = + | "to_generate" + | "to_interpolate" + | "to_upscale" + | "completed" + | "error" + +export type ClapAuthor = + | "auto" // the element was edited automatically using basic if/else logical rules + | "ai" // the element was edited using a large language model + | "human" // the element was edited by a human + +export type ClapAssetSource = + | "REMOTE" // http:// or https:// + + // note that "path" assets are potentially a security risk, they need to be treated with care + | "PATH" // a file path eg. /path or ./path/to/ or ../path/to/ + + | "DATA" // a data URI, starting with data: + + | "PROMPT" // by default, a plain text prompt + + | "EMPTY" + +export type ClapModelGender = + | "male" + | "female" + | "person" + | "object" + +export type ClapModelAppearance = "serious" | "neutral" | "friendly" | "chill" + +// this is used for accent, style.. +export type ClapModelRegion = + | "american" + | "british" + | "australian" + | "canadian" + | "indian" + | "french" + | "italian" + | "german" + | "chinese" + +// note: this is all very subjective, so please use good judgment +// +// "deep" might indicate a deeper voice tone, thicker, rich in harmonics +// in this context, it is used to indicate voices that could +// be associated with African American (AADOS) characters +// +// "high" could be used for some other countries, eg. asia +export type ClapModelTimbre = "high" | "neutral" | "deep" + +export type ClapVoiceVendor = "ElevenLabs" | "XTTS" + +export type ClapVoice = { + name: string + gender: ClapModelGender + age: number + region: ClapModelRegion + timbre: ClapModelTimbre + appearance: ClapModelAppearance + voiceVendor: ClapVoiceVendor + voiceId: string +} + +export type ClapHeader = { + format: "clap-0" + numberOfModels: number + numberOfScenes: number + numberOfSegments: number +} + +export type ClapMeta = { + id: string + title: string + description: string + synopsis: string + licence: string + orientation: string + + // the default duration of the experience + // the real one might last longer if made interactive + durationInMs: number + + width: number + height: number + defaultVideoModel: string + extraPositivePrompt: string[] + screenplay: string + isLoop: boolean + isInteractive: boolean +} + +export type ClapSceneEvent = { + id: string + type: "description" | "dialogue" | "action" + character?: string + description: string + behavior: string + startAtLine: number + endAtLine: number +} + +export type ClapScene = { + id: string + scene: string + line: string + rawLine: string + sequenceFullText: string + sequenceStartAtLine: number + sequenceEndAtLine: number + startAtLine: number + endAtLine: number + events: ClapSceneEvent[] +} + +export type ClapSegment = { + id: string + track: number + startTimeInMs: number + endTimeInMs: number + category: ClapSegmentCategory + modelId: string + sceneId: string + prompt: string + label: string + outputType: ClapOutputType + renderId: string + status: ClapSegmentStatus + assetUrl: string + assetDurationInMs: number + createdBy: ClapAuthor + editedBy: ClapAuthor + outputGain: number + seed: number +} + +export type ClapModel = { + id: string + category: ClapSegmentCategory + triggerName: string + label: string + description: string + author: string + thumbnailUrl: string + seed: number + + assetSourceType: ClapAssetSource + assetUrl: string + + // those are only used by certain types of models + age: number + gender: ClapModelGender + region: ClapModelRegion + appearance: ClapModelAppearance + voiceVendor: ClapVoiceVendor + voiceId: string +} + +export type ClapProject = { + meta: ClapMeta + models: ClapModel[] + scenes: ClapScene[] + segments: ClapSegment[] + // let's keep room for other stuff (screenplay etc) +} diff --git a/src/core/converters/blobToWebp.mts b/src/core/converters/blobToWebp.mts new file mode 100644 index 0000000000000000000000000000000000000000..4a194a50a65b71ed9c056b31e6a81af6b27ba245 --- /dev/null +++ b/src/core/converters/blobToWebp.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function blobToWebp(blob: Blob) { + return addBase64Header(Buffer.from(await blob.text()).toString('base64'), "webp") +} \ No newline at end of file diff --git a/src/core/converters/bufferToJpeg.mts b/src/core/converters/bufferToJpeg.mts new file mode 100644 index 0000000000000000000000000000000000000000..4b7f6835b4c91992dbb8d1f0ac8b2e05df088746 --- /dev/null +++ b/src/core/converters/bufferToJpeg.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function bufferToJpeg(buffer: Buffer) { + return addBase64Header(buffer.toString('base64'), "jpeg") +} \ No newline at end of file diff --git a/src/core/converters/bufferToMp3.mts b/src/core/converters/bufferToMp3.mts new file mode 100644 index 0000000000000000000000000000000000000000..0ea8cbb26d35cc50dfbdca18d637c3d49f669b93 --- /dev/null +++ b/src/core/converters/bufferToMp3.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function bufferToMp3(buffer: Buffer) { + return addBase64Header(buffer.toString('base64'), "mp3") +} \ No newline at end of file diff --git a/src/core/converters/bufferToMp4.mts b/src/core/converters/bufferToMp4.mts new file mode 100644 index 0000000000000000000000000000000000000000..64e9a102bba579451ad0ce595fced48ee633c1b9 --- /dev/null +++ b/src/core/converters/bufferToMp4.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function bufferToMp4(buffer: Buffer) { + return addBase64Header(buffer.toString('base64'), "mp4") +} \ No newline at end of file diff --git a/src/core/converters/bufferToPng.mts b/src/core/converters/bufferToPng.mts new file mode 100644 index 0000000000000000000000000000000000000000..0c18a39d9182b5d2c9fa0b9bbda7e282ec100ac5 --- /dev/null +++ b/src/core/converters/bufferToPng.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function bufferToPng(buffer: Buffer) { + return addBase64Header(buffer.toString('base64'), "png") +} \ No newline at end of file diff --git a/src/core/converters/bufferToWav.mts b/src/core/converters/bufferToWav.mts new file mode 100644 index 0000000000000000000000000000000000000000..bf083aac9a7c8dcf2184e8b2f523048006fb4b77 --- /dev/null +++ b/src/core/converters/bufferToWav.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function bufferToWav(buffer: Buffer) { + return addBase64Header(buffer.toString('base64'), "wav") +} \ No newline at end of file diff --git a/src/core/converters/bufferToWebp.mts b/src/core/converters/bufferToWebp.mts new file mode 100644 index 0000000000000000000000000000000000000000..430730b2ffc439363aac29fb198df9815deb4c9b --- /dev/null +++ b/src/core/converters/bufferToWebp.mts @@ -0,0 +1,5 @@ +import { addBase64Header } from "../base64/addBase64.mts"; + +export async function bufferToWebp(buffer: Buffer) { + return addBase64Header(buffer.toString('base64'), "webp") +} \ No newline at end of file diff --git a/src/core/converters/convertImageTo.mts b/src/core/converters/convertImageTo.mts new file mode 100644 index 0000000000000000000000000000000000000000..5909c27a111561fef6c0ea7a8d029b194d90f065 --- /dev/null +++ b/src/core/converters/convertImageTo.mts @@ -0,0 +1,31 @@ +import { convertImageToJpeg } from "./convertImageToJpeg.mts" +import { convertImageToPng } from "./convertImageToPng.mts" +import { convertImageToWebp } from "./convertImageToWebp.mts" +import { ImageFileExt } from "./imageFormats.mts" + +/** + * Convert an image to one of the supported file formats + * + * @param imgBase64 + * @param outputFormat + * @returns + */ +export async function convertImageTo(imgBase64: string = "", outputFormat: ImageFileExt): Promise { + const format = outputFormat.trim().toLowerCase() as ImageFileExt + if (!["jpeg", "jpg", "png", "webp"].includes(format)) { + throw new Error(`unsupported file format "${format}"`) + } + + const isJpeg = format === "jpg" || format === "jpeg" + + + if (isJpeg) { + return convertImageToJpeg(imgBase64) + } + + if (format === "webp") { + return convertImageToWebp(imgBase64) + } + + return convertImageToPng(imgBase64) +} diff --git a/src/core/converters/convertImageToJpeg.mts b/src/core/converters/convertImageToJpeg.mts new file mode 100644 index 0000000000000000000000000000000000000000..7ef63e91a8b83873d9a088c5483d0c1bc4d3d454 --- /dev/null +++ b/src/core/converters/convertImageToJpeg.mts @@ -0,0 +1,27 @@ +import sharp from "sharp" + +export async function convertImageToJpeg(imgBase64: string = "", quality: number = 92): Promise { + + const base64WithoutHeader = imgBase64.split(";base64,")[1] || "" + + if (!base64WithoutHeader) { + const slice = `${imgBase64 || ""}`.slice(0, 50) + throw new Error(`couldn't process input image "${slice}..."`) + } + + // Convert base64 to buffer + const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64') + + // Resize the buffer to the target size + const newBuffer = await sharp(tmpBuffer) + .jpeg({ + quality, + // we don't use progressive: true because we pre-load images anyway + }) + .toBuffer() + + // Convert the buffer back to base64 + const newImageBase64 = newBuffer.toString('base64') + + return `data:image/jpeg;base64,${newImageBase64}` +} \ No newline at end of file diff --git a/src/core/converters/convertImageToOriginal.mts b/src/core/converters/convertImageToOriginal.mts new file mode 100644 index 0000000000000000000000000000000000000000..92971f64b13d98f8700c5b76190bfa6925c47beb --- /dev/null +++ b/src/core/converters/convertImageToOriginal.mts @@ -0,0 +1,6 @@ + +// you are reading it right: this function does.. nothing! +// it is a NOOP conversion function +export async function convertImageToOriginal(imgBase64: string = ""): Promise { + return imgBase64 +} \ No newline at end of file diff --git a/src/core/converters/convertImageToPng.mts b/src/core/converters/convertImageToPng.mts new file mode 100644 index 0000000000000000000000000000000000000000..8edd35bc9b728d814134dea7a5b70bd06b8f83a9 --- /dev/null +++ b/src/core/converters/convertImageToPng.mts @@ -0,0 +1,23 @@ +import sharp from "sharp" + +export async function convertImageToPng(imgBase64: string = ""): Promise { + + const base64WithoutHeader = imgBase64.split(";base64,")[1] || "" + + if (!base64WithoutHeader) { + const slice = `${imgBase64 || ""}`.slice(0, 50) + throw new Error(`couldn't process input image "${slice}..."`) + } + + // Convert base64 to buffer + const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64') + + const newBuffer = await sharp(tmpBuffer) + .png() + .toBuffer() + + // Convert the buffer back to base64 + const newImageBase64 = newBuffer.toString('base64') + + return `data:image/png;base64,${newImageBase64}` +} \ No newline at end of file diff --git a/src/core/converters/convertImageToWebp.mts b/src/core/converters/convertImageToWebp.mts new file mode 100644 index 0000000000000000000000000000000000000000..2d936faa021aca1b7bafc3847833d69041fabe9d --- /dev/null +++ b/src/core/converters/convertImageToWebp.mts @@ -0,0 +1,41 @@ +import sharp from "sharp" + +export async function convertImageToWebp(imgBase64: string = ""): Promise { + + const base64WithoutHeader = imgBase64.split(";base64,")[1] || "" + + if (!base64WithoutHeader) { + const slice = `${imgBase64 || ""}`.slice(0, 50) + throw new Error(`couldn't process input image "${slice}..."`) + } + + // Convert base64 to buffer + const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64') + + // Resize the buffer to the target size + const newBuffer = await sharp(tmpBuffer) + .webp({ + // for options please see https://sharp.pixelplumbing.com/api-output#webp + + // preset: "photo", + + // effort: 3, + + // for a PNG-like quality + // lossless: true, + + // by default it is quality 80 + quality: 80, + + // nearLossless: true, + + // use high quality chroma subsampling + smartSubsample: true, + }) + .toBuffer() + + // Convert the buffer back to base64 + const newImageBase64 = newBuffer.toString('base64') + + return `data:image/webp;base64,${newImageBase64}` +} \ No newline at end of file diff --git a/src/core/converters/htmlToBase64Png.mts b/src/core/converters/htmlToBase64Png.mts new file mode 100644 index 0000000000000000000000000000000000000000..13a7d35e45e028232984451c7d67ecd583f9cbc1 --- /dev/null +++ b/src/core/converters/htmlToBase64Png.mts @@ -0,0 +1,78 @@ +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" + +import { v4 as uuidv4 } from "uuid" +import puppeteer from "puppeteer" + +export async function htmlToBase64Png({ + outputImagePath, + html, + width = 800, + height = 600, +}: { + outputImagePath?: string + html?: string + width?: number + height: number +}): Promise<{ + filePath: string + buffer: Buffer +}> { + + // If no output path is provided, create a temporary file for output + if (!outputImagePath) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), uuidv4())) + + outputImagePath = path.join(tempDir, `${uuidv4()}.png`) + } + + const browser = await puppeteer.launch({ + headless: "new", + + // apparently we need those, see: + // https://unix.stackexchange.com/questions/694734/puppeteer-in-alpine-docker-with-chromium-headless-dosent-seems-to-work + executablePath: '/usr/bin/chromium-browser', + args: [ + '--no-sandbox', + '--headless', + '--disable-gpu', + '--disable-dev-shm-usage' + ] + }) + + const page = await browser.newPage() + + page.setViewport({ + width, + height, + }) + + try { + await page.setContent(html) + + const content = await page.$("body") + + const buffer = await content.screenshot({ + path: outputImagePath, + omitBackground: true, + captureBeyondViewport: false, + + // we must keep PNG here, if we want transparent backgrounds + type: "png", + + // we should leave it to binary (the default value) if we save to a file + // encoding: "binary", // "base64", + }) + + return { + filePath: outputImagePath, + buffer + } + } catch (err) { + throw err + } finally { + await page.close() + await browser.close() + } +}; \ No newline at end of file diff --git a/src/core/converters/imageFormats.mts b/src/core/converters/imageFormats.mts new file mode 100644 index 0000000000000000000000000000000000000000..027dfd62d46e180fc8d06fbaf5ac232af90e3008 --- /dev/null +++ b/src/core/converters/imageFormats.mts @@ -0,0 +1 @@ +export type ImageFileExt = "png" | "jpeg" | "jpg" | "webp" diff --git a/src/core/ffmpeg/addImageToVideo.mts b/src/core/ffmpeg/addImageToVideo.mts new file mode 100644 index 0000000000000000000000000000000000000000..1660d098f6de8bfcce392c18c3747ce473987fe8 --- /dev/null +++ b/src/core/ffmpeg/addImageToVideo.mts @@ -0,0 +1,50 @@ +import { promises as fs, existsSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import ffmpeg from "fluent-ffmpeg"; +import { v4 as uuidv4 } from "uuid"; + +type AddImageToVideoParams = { + inputVideoPath: string; + inputImagePath: string; + outputVideoPath?: string; +}; + +export async function addImageToVideo({ + inputVideoPath, + inputImagePath, + outputVideoPath, +}: AddImageToVideoParams): Promise { + // Verify that the input files exist + if (!existsSync(inputVideoPath)) { + throw new Error(`Input video file does not exist: ${inputVideoPath}`); + } + if (!existsSync(inputImagePath)) { + throw new Error(`Input image file does not exist: ${inputImagePath}`); + } + + // If no output path is provided, create a temporary file for output + if (!outputVideoPath) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), uuidv4())); + outputVideoPath = path.join(tempDir, `${uuidv4()}.mp4`); + } + + // Return a promise that resolves with the path to the output video + return new Promise((resolve, reject) => { + ffmpeg(inputVideoPath) + .input(inputImagePath) + .complexFilter([ + { + filter: "overlay", + options: { x: "0", y: "0" }, // Overlay on the entire video frame + } + ]) + .on("error", (err) => { + reject(new Error(`Error processing video: ${err.message}`)); + }) + .on("end", () => { + resolve(outputVideoPath); + }) + .save(outputVideoPath); + }); +} diff --git a/src/core/ffmpeg/addTextToVideo.mts b/src/core/ffmpeg/addTextToVideo.mts new file mode 100644 index 0000000000000000000000000000000000000000..32fab3bb843786843f19186d2b3ba2a2a4cc73c1 --- /dev/null +++ b/src/core/ffmpeg/addTextToVideo.mts @@ -0,0 +1,23 @@ +import { createTextOverlayImage } from "./createTextOverlayImage.mts"; +import { addImageToVideo } from "./addImageToVideo.mts"; + +export async function addTextToVideo() { + + const inputVideoPath = "/Users/jbilcke/Downloads/use_me.mp4" + + const { filePath } = await createTextOverlayImage({ + text: "This tech is hot πŸ₯΅", + width: 1024 , + height: 576, + }) + console.log("filePath:", filePath) + + /* + const pathToVideo = await addImageToVideo({ + inputVideoPath, + inputImagePath: filePath, + }) + + console.log("pathToVideo:", pathToVideo) + */ +} \ No newline at end of file diff --git a/src/core/ffmpeg/concatenateAudio.mts b/src/core/ffmpeg/concatenateAudio.mts new file mode 100644 index 0000000000000000000000000000000000000000..c073aee15c79a707534973e1493e4c31852600f2 --- /dev/null +++ b/src/core/ffmpeg/concatenateAudio.mts @@ -0,0 +1,122 @@ +import { existsSync, promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" + +import { v4 as uuidv4 } from "uuid"; +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; +import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"; +import { getMediaInfo } from "./getMediaInfo.mts"; +import { removeTemporaryFiles } from "../files/removeTmpFiles.mts"; +import { addBase64Header } from "../base64/addBase64.mts"; + +export type ConcatenateAudioOptions = { + // those are base64 audio strings! + audioTracks?: string[]; // base64 + audioFilePaths?: string[]; // path + crossfadeDurationInSec?: number; + outputFormat?: string; // "wav" or "mp3" + output?: string; +} + +export type ConcatenateAudioOutput = { + filepath: string; + durationInSec: number; +} + +export async function concatenateAudio({ + output, + audioTracks = [], + audioFilePaths = [], + crossfadeDurationInSec = 10, + outputFormat = "wav" +}: ConcatenateAudioOptions): Promise { + if (!Array.isArray(audioTracks)) { + throw new Error("Audios must be provided in an array"); + } + + const tempDir = path.join(os.tmpdir(), uuidv4()); + await fs.mkdir(tempDir); + + // console.log(" |- created tmp dir") + + // trivial case: there is only one audio to concatenate! + if (audioTracks.length === 1 && audioTracks[0]) { + const audioTrack = audioTracks[0] + const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`); + await writeBase64ToFile(addBase64Header(audioTrack, "wav"), outputFilePath); + + // console.log(" |- there is only one track! so.. returning that") + const { durationInSec } = await getMediaInfo(outputFilePath); + return { filepath: outputFilePath, durationInSec }; + } + + if (audioFilePaths.length === 1) { + throw new Error("concatenating a single audio file path is not implemented yet") + } + + try { + + let i = 0 + for (const track of audioTracks) { + if (!track) { continue } + const audioFilePath = path.join(tempDir, `audio_${++i}.wav`); + await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath); + audioFilePaths.push(audioFilePath); + } + + audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio)) + + const outputFilePath = output ?? path.join(tempDir, `${uuidv4()}.${outputFormat}`); + + let filterComplex = ""; + let prevLabel = "0"; + + for (let i = 0; i < audioFilePaths.length - 1; i++) { + const nextLabel = `a${i}`; + filterComplex += `[${prevLabel}][${i + 1}]acrossfade=d=${crossfadeDurationInSec}:c1=tri:c2=tri[${nextLabel}];`; + prevLabel = nextLabel; + } + + + console.log(" |- concatenateAudio(): DEBUG:", { + tempDir, + audioFilePaths, + outputFilePath, + filterComplex, + prevLabel + }) + + let cmd: FfmpegCommand = ffmpeg() // .outputOptions('-vn'); + + audioFilePaths.forEach((audio, i) => { + cmd = cmd.input(audio); + }); + + + const promise = new Promise((resolve, reject) => { + cmd = cmd + .on('error', reject) + .on('end', async () => { + try { + const { durationInSec } = await getMediaInfo(outputFilePath); + // console.log("concatenation ended! see ->", outputFilePath) + resolve({ filepath: outputFilePath, durationInSec }); + } catch (err) { + reject(err); + } + }) + .complexFilter(filterComplex, prevLabel) + .save(outputFilePath); + }); + + const result = await promise + + return result + } catch (error) { + console.error(`Failed to assemble audio!`) + console.error(error) + throw new Error(`Failed to assemble audio: ${(error as Error)?.message || error}`); + } finally { + await removeTemporaryFiles(audioFilePaths) + } +} diff --git a/src/core/ffmpeg/concatenateVideos.mts b/src/core/ffmpeg/concatenateVideos.mts new file mode 100644 index 0000000000000000000000000000000000000000..b1408b5e816e1ed4f426fb8f037eb290ff7907a3 --- /dev/null +++ b/src/core/ffmpeg/concatenateVideos.mts @@ -0,0 +1,61 @@ +import { existsSync, promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { v4 as uuidv4 } from "uuid"; +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; + +import { getMediaInfo } from "./getMediaInfo.mts"; + +export type ConcatenateVideoOutput = { + filepath: string; + durationInSec: number; +} + +export async function concatenateVideos({ + output, + videoFilePaths = [], +}: { + output?: string; + + // those are videos PATHs, not base64 strings! + videoFilePaths: string[]; +}): Promise { + if (!Array.isArray(videoFilePaths)) { + throw new Error("Videos must be provided in an array"); + } + + videoFilePaths = videoFilePaths.filter((videoPath) => existsSync(videoPath)) + + // Create a temporary working directory + const tempDir = path.join(os.tmpdir(), uuidv4()); + await fs.mkdir(tempDir); + + const filePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`); + + if (!filePath) { + throw new Error("Failed to generate a valid temporary file path"); + } + + let cmd: FfmpegCommand = ffmpeg(); + + videoFilePaths.forEach((video) => { + cmd = cmd.addInput(video) + }) + + return new Promise<{ filepath: string; durationInSec: number }>( + (resolve, reject) => { + cmd + .on('error', reject) + .on('end', async () => { + try { + const { durationInSec } = await getMediaInfo(filePath); + resolve({ filepath: filePath, durationInSec }); + } catch (err) { + reject(err); + } + }) + .mergeToFile(filePath, tempDir); + } + ); +}; diff --git a/src/core/ffmpeg/concatenateVideosAndMergeAudio.mts b/src/core/ffmpeg/concatenateVideosAndMergeAudio.mts new file mode 100644 index 0000000000000000000000000000000000000000..44b32dc26bc130b348bc9140fe84420bcf43f284 --- /dev/null +++ b/src/core/ffmpeg/concatenateVideosAndMergeAudio.mts @@ -0,0 +1,130 @@ +import { existsSync, promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" + +import { v4 as uuidv4 } from "uuid"; +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; +import { concatenateVideos } from "./concatenateVideos.mts"; +import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"; +import { getMediaInfo } from "./getMediaInfo.mts"; +import { removeTemporaryFiles } from "../files/removeTmpFiles.mts"; +import { addBase64Header } from "../base64/addBase64.mts"; + +type ConcatenateVideoAndMergeAudioOptions = { + output?: string; + audioTracks?: string[]; // base64 + audioFilePaths?: string[]; // path + videoTracks?: string[]; // base64 + videoFilePaths?: string[]; // path +}; + +export type ConcatenateVideoAndMergeAudioOutput = { + filepath: string; + durationInSec: number; +} + +// note: the audio tracks will be fused together, as in "mixed" +// this return a path to the file +export const concatenateVideosAndMergeAudio = async ({ + output, + audioTracks = [], + audioFilePaths = [], + videoTracks = [], + videoFilePaths = [] +}: ConcatenateVideoAndMergeAudioOptions): Promise => { + + try { + // Prepare temporary directories + const tempDir = path.join(os.tmpdir(), uuidv4()); + await fs.mkdir(tempDir); + + let i = 0 + for (const track of audioTracks) { + if (!track) { continue } + const audioFilePath = path.join(tempDir, `audio${++i}.wav`); + await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath); + audioFilePaths.push(audioFilePath); + } + audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio)) + + + // Decode and concatenate base64 video tracks to temporary file + i = 0 + for (const track of videoTracks) { + if (!track) { continue } + const videoFilePath = path.join(tempDir, `video${++i}.mp4`); + + await writeBase64ToFile(addBase64Header(track, "mp4"), videoFilePath); + + videoFilePaths.push(videoFilePath); + } + videoFilePaths = videoFilePaths.filter((video) => existsSync(video)) + + // The final output file path + const finalOutputFilePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`); + + /* + console.log("DEBUG:", { + tempDir, + audioFilePath, + audioTrack: audioTrack.slice(0, 40), + videoTracks: videoTracks.map(vid => vid.slice(0, 40)), + videoFilePaths, + finalOutputFilePath + }) + */ + + // console.log("concatenating videos (without audio)..") + const tempFilePath = await concatenateVideos({ + videoFilePaths, + }) + // console.log("concatenated silent shots to: ", tempFilePath) + + // console.log("concatenating video + audio..") + + // Add audio to the concatenated video file + const promise = new Promise((resolve, reject) => { + let cmd = ffmpeg().addInput(tempFilePath.filepath).outputOptions("-c:v copy"); + + for (const audioFilePath of audioFilePaths) { + cmd = cmd.addInput(audioFilePath); + } + + if (audioFilePaths.length) { + // Mix all audio tracks (if there are any) into a single stereo stream + const mixFilter = audioFilePaths.map((_, index) => `[${index + 1}:a]`).join('') + `amix=inputs=${audioFilePaths.length}:duration=first[outa]`; + cmd = cmd + .complexFilter(mixFilter) + .outputOptions([ + "-map", "0:v:0", // Maps the video stream from the first input (index 0) as the output video stream + "-map", "[outa]", // Maps the labeled audio output from the complex filter (mixed audio) as the output audio stream + "-c:a aac", // Specifies the audio codec to be AAC (Advanced Audio Coding) + "-shortest" // Ensures the output file's duration equals the shortest input stream's duration + ]); + } else { + // If there are no audio tracks, just map the video + cmd = cmd.outputOptions(["-map", "0:v:0"]); + } + + cmd = cmd + .on("error", reject) + .on('end', async () => { + try { + const { durationInSec } = await getMediaInfo(finalOutputFilePath); + resolve({ filepath: finalOutputFilePath, durationInSec }); + } catch (err) { + reject(err); + } + }) + .saveToFile(finalOutputFilePath); + }); + + const result = await promise; + + return result + } catch (error) { + throw new Error(`Failed to assemble video: ${(error as Error).message}`); + } finally { + await removeTemporaryFiles([...videoFilePaths, ...audioFilePaths]) + } +}; \ No newline at end of file diff --git a/src/core/ffmpeg/concatenateVideosWithAudio.mts b/src/core/ffmpeg/concatenateVideosWithAudio.mts new file mode 100644 index 0000000000000000000000000000000000000000..c7c96c479ee3f29e883b3a54be35714d9a4134ca --- /dev/null +++ b/src/core/ffmpeg/concatenateVideosWithAudio.mts @@ -0,0 +1,158 @@ +import { existsSync, promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" + +import { v4 as uuidv4 } from "uuid"; +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; +import { concatenateVideos } from "./concatenateVideos.mts"; +import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"; +import { getMediaInfo } from "./getMediaInfo.mts"; +import { removeTemporaryFiles } from "../files/removeTmpFiles.mts"; +import { addBase64Header } from "../base64/addBase64.mts"; + +type ConcatenateVideoWithAudioOptions = { + output?: string; + audioTrack?: string; // base64 + audioFilePath?: string; // path + videoTracks?: string[]; // base64 + videoFilePaths?: string[]; // path + videoTracksVolume?: number; // Represents the volume level of the original video track + audioTrackVolume?: number; // Represents the volume level of the additional audio track + asBase64?: boolean; +}; + + +export const concatenateVideosWithAudio = async ({ + output, + audioTrack = "", + audioFilePath = "", + videoTracks = [], + videoFilePaths = [], + videoTracksVolume = 0.5, // (1.0 = 100% volume) + audioTrackVolume = 0.5, + asBase64 = false, +}: ConcatenateVideoWithAudioOptions): Promise => { + + try { + // Prepare temporary directories + const tempDir = path.join(os.tmpdir(), uuidv4()); + await fs.mkdir(tempDir); + + if (audioTrack) { + audioFilePath = path.join(tempDir, `audio.wav`); + await writeBase64ToFile(addBase64Header(audioTrack, "wav"), audioFilePath); + } + + // Decode and concatenate base64 video tracks to temporary file + let i = 0 + for (const track of videoTracks) { + if (!track) { continue } + const videoFilePath = path.join(tempDir, `video${++i}.mp4`); + + await writeBase64ToFile(addBase64Header(track, "mp4"), videoFilePath); + + videoFilePaths.push(videoFilePath); + } + + videoFilePaths = videoFilePaths.filter((video) => existsSync(video)) + + // console.log("concatenating videos (without audio)..") + const tempFilePath = await concatenateVideos({ + videoFilePaths, + }) + + // Check if the concatenated video has audio or not + const tempMediaInfo = await getMediaInfo(tempFilePath.filepath); + const hasOriginalAudio = tempMediaInfo.hasAudio; + + const finalOutputFilePath = output || path.join(tempDir, `${uuidv4()}.mp4`); + + // Begin ffmpeg command configuration + let cmd = ffmpeg(); + + // Add silent concatenated video + cmd = cmd.addInput(tempFilePath.filepath); + + // If additional audio is provided, add audio to ffmpeg command + if (audioFilePath) { + cmd = cmd.addInput(audioFilePath); + // If the input video already has audio, we will mix it with additional audio + if (hasOriginalAudio) { + const filterComplex = ` + [0:a]volume=${videoTracksVolume}[a0]; + [1:a]volume=${audioTrackVolume}[a1]; + [a0][a1]amix=inputs=2:duration=shortest[a] + `.trim(); + + cmd = cmd.outputOptions([ + '-filter_complex', filterComplex, + '-map', '0:v', + '-map', '[a]', + '-c:v', 'copy', + '-c:a', 'aac', + ]); + } else { + // If the input video has no audio, just use the additional audio as is + cmd = cmd.outputOptions([ + '-map', '0:v', + '-map', '1:a', + '-c:v', 'copy', + '-c:a', 'aac', + ]); + } + } else { + // If no additional audio is provided, simply copy the video stream + cmd = cmd.outputOptions([ + '-c:v', 'copy', + hasOriginalAudio ? '-c:a' : '-an', // If original audio exists, copy it; otherwise, indicate no audio + ]); + } + + /* + console.log("DEBUG:", { + videoTracksVolume, + audioTrackVolume, + videoFilePaths, + tempFilePath, + hasOriginalAudio, + // originalAudioVolume, + audioFilePath, + // additionalAudioVolume, + finalOutputFilePath + }) + */ + + // Set up event handlers for ffmpeg processing + const promise = new Promise((resolve, reject) => { + cmd.on('error', (err) => { + console.error(" Error during ffmpeg processing:", err.message); + reject(err); + }).on('end', async () => { + // When ffmpeg finishes processing, resolve the promise with file info + try { + if (asBase64) { + try { + const outputBuffer = await fs.readFile(finalOutputFilePath); + const outputBase64 = addBase64Header(outputBuffer.toString("base64"), "mp4") + resolve(outputBase64); + } catch (error) { + reject(new Error(`Error reading output video file: ${(error as Error).message}`)); + } + } else { + resolve(finalOutputFilePath) + } + } catch (err) { + reject(err); + } + }).save(finalOutputFilePath); // Provide the path where to save the file + }); + + // Wait for ffmpeg to complete the process + const result = await promise; + return result; + } catch (error) { + throw new Error(`Failed to assemble video: ${(error as Error).message}`); + } finally { + await removeTemporaryFiles([...videoFilePaths].concat(audioFilePath)) + } +}; \ No newline at end of file diff --git a/src/core/ffmpeg/convertAudioToWav.mts b/src/core/ffmpeg/convertAudioToWav.mts new file mode 100644 index 0000000000000000000000000000000000000000..17602abe187cdef6a84cbd5b391f7af9d4895105 --- /dev/null +++ b/src/core/ffmpeg/convertAudioToWav.mts @@ -0,0 +1,69 @@ +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import ffmpeg from "fluent-ffmpeg"; +import { Buffer } from "node:buffer"; + +type ConvertAudioToWavParams = { + input: string; + outputAudioPath?: string; + asBase64?: boolean; +}; + +export async function convertAudioToWav({ + input, + outputAudioPath, + asBase64 = false, +}: ConvertAudioToWavParams): Promise { + let inputAudioPath = input; + + // Check if the input is a base64 string + if (input.startsWith("data:")) { + const matches = input.match(/^data:audio\/(mp3|wav);base64,(.+)$/); + + if (!matches) { + throw new Error("Invalid base64 audio data"); + } + + const inputBuffer = Buffer.from(matches[2], "base64"); + const inputFormat = matches[1]; // Either 'mp3' or 'wav' + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-input-")); + inputAudioPath = path.join(tempDir, `temp.${inputFormat}`); + + // Write the base64 data to the temporary file + await fs.writeFile(inputAudioPath, inputBuffer); + } else { + // Verify that the input file exists + if (!(await fs.stat(inputAudioPath)).isFile()) { + throw new Error(`Input audio file does not exist: ${inputAudioPath}`); + } + } + + // If no output path is provided, create a temporary file for the output + if (!outputAudioPath) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-output-")); + outputAudioPath = path.join(tempDir, `${path.parse(inputAudioPath).name}.wav`); + } + + return new Promise((resolve, reject) => { + ffmpeg(inputAudioPath) + .toFormat("wav") + .on("error", (err) => { + reject(new Error(`Error converting audio to WAV: ${err.message}`)); + }) + .on("end", async () => { + if (asBase64) { + try { + const audioBuffer = await fs.readFile(outputAudioPath); + const audioBase64 = `data:audio/wav;base64,${audioBuffer.toString("base64")}`; + resolve(audioBase64); + } catch (error) { + reject(new Error(`Error reading audio file: ${(error as Error).message}`)); + } + } else { + resolve(outputAudioPath); + } + }) + .save(outputAudioPath); + }); +} \ No newline at end of file diff --git a/src/core/ffmpeg/convertMp4ToMp3.mts b/src/core/ffmpeg/convertMp4ToMp3.mts new file mode 100644 index 0000000000000000000000000000000000000000..576c6b03114df6236252c783ee919f772391e392 --- /dev/null +++ b/src/core/ffmpeg/convertMp4ToMp3.mts @@ -0,0 +1,65 @@ + +import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import ffmpeg from "fluent-ffmpeg"; +import { tmpdir } from "node:os"; +import { Buffer } from "node:buffer"; + +export async function convertMp4ToMp3({ + input, + outputAudioPath, + asBase64 = false, +}: { + input: string; + outputAudioPath?: string; + asBase64?: boolean; +}): Promise { + let inputFilePath = input; + + // Check if the input is a base64 string + if (input.startsWith("data:")) { + const base64Data = input.split(",")[1]; + const inputBuffer = Buffer.from(base64Data, "base64"); + + // Create a temporary file for the input video + const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-input-")); + inputFilePath = path.join(tempDir, "temp.mp4"); + + // Write the base64 data to the temporary file + await writeFile(inputFilePath, inputBuffer); + } else { + // Verify that the input file exists + if (!(await stat(inputFilePath)).isFile()) { + throw new Error(`Input video file does not exist: ${inputFilePath}`); + } + } + + // If no output path is provided, create a temporary file for the output + if (!outputAudioPath) { + const tempDir = await mkdtemp(path.join(tmpdir(), "ffmpeg-output-")); + outputAudioPath = path.join(tempDir, `${path.parse(inputFilePath).name}.mp3`); + } + + return new Promise((resolve, reject) => { + ffmpeg(inputFilePath) + .toFormat("mp3") + .on("error", (err) => { + reject(new Error(`Error converting video to audio: ${err.message}`)); + }) + .on("end", async () => { + if (asBase64) { + try { + const audioBuffer = await readFile(outputAudioPath); + const audioBase64 = `data:audio/mp3;base64,${audioBuffer.toString("base64")}`; + resolve(audioBase64); + } catch (error) { + reject(new Error(`Error reading audio file: ${(error as Error).message}`)); + } + } else { + resolve(outputAudioPath); + } + }) + .save(outputAudioPath); + }); +} \ No newline at end of file diff --git a/src/core/ffmpeg/convertMp4ToWebm.mts b/src/core/ffmpeg/convertMp4ToWebm.mts new file mode 100644 index 0000000000000000000000000000000000000000..b5223ccaa5673b9946f901772af7c492c8ed2ef1 --- /dev/null +++ b/src/core/ffmpeg/convertMp4ToWebm.mts @@ -0,0 +1,70 @@ + +import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"; +import path from "node:path"; + +import { tmpdir } from "node:os"; +import { Buffer } from "node:buffer"; + +import ffmpeg from "fluent-ffmpeg"; + +export async function convertMp4ToWebm({ + input, + outputVideoPath, + asBase64 = false, +}: { + input: string; + outputVideoPath?: string; + asBase64?: boolean; +}): Promise { + let inputFilePath = input; + + // Check if the input is a base64 string + if (input.startsWith("data:")) { + const base64Data = input.split(",")[1]; + const inputBuffer = Buffer.from(base64Data, "base64"); + + // Create a temporary file for the input video + const tempDir = await mkdtemp(path.join(tmpdir(), "ffmpeg-input-")); + inputFilePath = path.join(tempDir, "temp.mp4"); + + // Write the base64 data to the temporary file + await writeFile(inputFilePath, inputBuffer); + } else { + // Verify that the input file exists + const inputFileStats = await stat(inputFilePath); + if (!inputFileStats.isFile()) { + throw new Error(`Input video file does not exist: ${inputFilePath}`); + } + } + + // If no output path is provided, create a temporary file for the output + if (!outputVideoPath) { + const tempDir = await mkdtemp(path.join(tmpdir(), "ffmpeg-output-")); + outputVideoPath = path.join(tempDir, `${path.parse(inputFilePath).name}.webm`); + } + + return new Promise((resolve, reject) => { + ffmpeg(inputFilePath) + .toFormat("webm") + .videoCodec("libvpx") + .addOption("-b:v", "1000k") // ~ 400 kB for 3 seconds of video + .audioCodec("libvorbis") + .on("error", (err) => { + reject(new Error(`Error converting video to WebM: ${err.message}`)); + }) + .on("end", async () => { + if (asBase64) { + try { + const videoBuffer = await readFile(outputVideoPath); + const videoBase64 = `data:video/webm;base64,${videoBuffer.toString("base64")}`; + resolve(videoBase64); + } catch (error) { + reject(new Error(`Error reading video file: ${(error as Error).message}`)); + } + } else { + resolve(outputVideoPath); + } + }) + .save(outputVideoPath); + }); +} \ No newline at end of file diff --git a/src/core/ffmpeg/createTextOverlayImage.mts b/src/core/ffmpeg/createTextOverlayImage.mts new file mode 100644 index 0000000000000000000000000000000000000000..88808f40ff79dc5bfe510832b7f5c19603b9f1fc --- /dev/null +++ b/src/core/ffmpeg/createTextOverlayImage.mts @@ -0,0 +1,57 @@ + +import { TextOverlayFont, TextOverlayFontWeight, TextOverlayStyle, getCssStyle } from "../utils/getCssStyle.mts" +import { htmlToBase64Png } from "../converters/htmlToBase64Png.mts" + +// generate a PNG overlay using HTML +export async function createTextOverlayImage({ + text = "", + textStyle = "outline", + fontFamily = "Montserrat", + fontSize = 10, + fontWeight = 600, + rotation = 0, + width = 1024, + height = 576 +}: { + text?: string + textStyle?: TextOverlayStyle + fontFamily?: TextOverlayFont + fontSize?: number + fontWeight?: TextOverlayFontWeight + rotation?: number + width?: number + height?: number +}): Promise<{ + filePath: string + buffer: Buffer +}> { + + + const html = ` + ${getCssStyle({ + fontFamily, + fontSize, + fontWeight: 600, + })} + + + +
+ + +

+ ${text} +

+
+ + +` + + const result = await htmlToBase64Png({ + html, + width, + height, + }) + + return result; +}; \ No newline at end of file diff --git a/src/core/ffmpeg/createVideoFromFrames.mts b/src/core/ffmpeg/createVideoFromFrames.mts new file mode 100644 index 0000000000000000000000000000000000000000..c02b47c1358767457ce902a5b195728f0c34fa85 --- /dev/null +++ b/src/core/ffmpeg/createVideoFromFrames.mts @@ -0,0 +1,173 @@ +import { promises as fs } from "node:fs" +import { writeFile, readFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +import ffmpeg from "fluent-ffmpeg" +import { v4 as uuidv4 } from "uuid" + +import { getMediaInfo } from "./getMediaInfo.mts" + +export async function createVideoFromFrames({ + inputFramesDirectory, + framesFilePattern, + outputVideoPath, + framesPerSecond = 25, + + // there isn't a lot of advantage for us to add film grain because: + // 1. I actually can't tell the different, probably because it's in HD, and so tiny + // 2. We want a neat "4K video from the 2020" look, not a quality from 30 years ago + // 3. grain has too much entropy and cannot be compressed, so it multiplies by 5 the size weight + grainAmount = 0, // Optional parameter for film grain (eg. 10) + + inputVideoToUseAsAudio, // Optional parameter for audio input (need to be a mp4, but it can be a base64 data URI or a file path) + + debug = false, + + asBase64 = false, +}: { + inputFramesDirectory: string; + + // the ffmpeg file pattern to use + framesFilePattern?: string; + + outputVideoPath?: string; + framesPerSecond?: number; + grainAmount?: number; // Values can range between 0 and higher for the desired amount + inputVideoToUseAsAudio?: string; // Optional parameter for audio input (need to be a mp4, but it can be a base64 data URI or a file path) + debug?: boolean; + asBase64?: boolean; +}): Promise { + // Ensure the input directory exists + await fs.access(inputFramesDirectory); + + + // Construct the input frame pattern + const inputFramePattern = path.join(inputFramesDirectory, framesFilePattern); + + + // Create a temporary working directory + const tempDir = path.join(os.tmpdir(), uuidv4()); + await fs.mkdir(tempDir); + + + let inputVideoToUseAsAudioFilePath = ""; + if (inputVideoToUseAsAudio.startsWith('data:')) { + // Extract the base64 content and decode it to a temporary file + const base64Content = inputVideoToUseAsAudio.split(';base64,').pop(); + if (!base64Content) { + throw new Error('Invalid base64 input provided'); + } + inputVideoToUseAsAudioFilePath = path.join(tempDir, `${uuidv4()}_audio_input.mp4`); + await writeFile(inputVideoToUseAsAudioFilePath, base64Content, 'base64'); + } else { + inputVideoToUseAsAudioFilePath = inputVideoToUseAsAudio; + } + + if (debug) { + console.log(" createVideoFromFraes(): inputVideoToUseAsAudioFilePath = ", inputVideoToUseAsAudioFilePath) + } + + + let canUseInputVideoForAudio = false + // Also, if provided, check that the audio source file exists + if (inputVideoToUseAsAudioFilePath) { + try { + await fs.access(inputVideoToUseAsAudioFilePath) + const info = await getMediaInfo(inputVideoToUseAsAudioFilePath) + if (info.hasAudio) { + canUseInputVideoForAudio = true + } + } catch (err) { + if (debug) { + console.log(" createVideoFromFrames(): warning: input video has no audio, so we are not gonna use that") + } + } + } + + const outputVideoFilePath = outputVideoPath ?? path.join(tempDir, `${uuidv4()}.mp4`); + + if (debug) { + console.log(" createVideoFromFrames(): outputOptions:", [ + // by default ffmpeg doesn't tell us why it fails to convet + // so we need to force it to spit everything out + "-loglevel", "debug", + + "-pix_fmt", "yuv420p", + "-c:v", "libx264", + "-r", `${framesPerSecond}`, + + // from ffmpeg doc: "Consider 17 or 18 to be visually lossless or nearly so; + // it should look the same or nearly the same as the input." + "-crf", "17", + ]) + } + + return new Promise((resolve, reject) => { + const command = ffmpeg() + .input(inputFramePattern) + .inputFPS(framesPerSecond) + .outputOptions([ + // by default ffmpeg doesn't tell us why it fails to convet + // so we need to force it to spit everything out + "-loglevel", "debug", + + "-pix_fmt", "yuv420p", + "-c:v", "libx264", + "-r", `${framesPerSecond}`, + "-crf", "18", + ]); + + + // If an input video for audio is provided, add it as an input for the ffmpeg command + if (canUseInputVideoForAudio) { + if (debug) { + console.log(" createVideoFromFrames(): adding audio as input:", inputVideoToUseAsAudioFilePath) + } + command.addInput(inputVideoToUseAsAudioFilePath); + command.outputOptions([ + "-map", "0:v", // Map video from the frames + "-map", "1:a", // Map audio from the input video + "-shortest" // Ensure output video duration is the shortest of the combined inputs + ]); + } + + // Apply grain effect using the geq filter if grainAmount is specified + if (grainAmount != null && grainAmount > 0) { + if (debug) { + console.log(" createVideoFromFrames(): adding grain:", grainAmount) + } + command.complexFilter([ + { + filter: "geq", + options: `lum='lum(X,Y)':cr='cr(X,Y)+(random(1)-0.5)*${grainAmount}':cb='cb(X,Y)+(random(1)-0.5)*${grainAmount}'` + } + ]); + } + + command.save(outputVideoFilePath) + .on("error", (err) => reject(err)) + .on("end", async () => { + if (debug) { + console.log(" createVideoFromFrames(): outputVideoFilePath: ", outputVideoFilePath) + } + if (!asBase64) { + resolve(outputVideoFilePath) + return + } + // Convert the output file to a base64 string + try { + const videoBuffer = await readFile(outputVideoFilePath); + const videoBase64 = `data:video/mp4;base64,${videoBuffer.toString('base64')}`; + console.log(" createVideoFromFrames(): output base64: ", videoBase64.slice(0, 120)) + resolve(videoBase64); + } catch (error) { + reject(new Error(`Error loading the video file: ${error}`)); + } finally { + // Clean up temporary files + await fs.rm(tempDir, { recursive: true }); + } + }); + }); +} + diff --git a/src/core/ffmpeg/cropBase64Video.mts b/src/core/ffmpeg/cropBase64Video.mts new file mode 100644 index 0000000000000000000000000000000000000000..e3bcdc74786617aa7e7a6e8d294a8232ac978da9 --- /dev/null +++ b/src/core/ffmpeg/cropBase64Video.mts @@ -0,0 +1,65 @@ +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import ffmpeg from "fluent-ffmpeg"; + +export async function cropBase64Video({ + base64Video, + width, + height, +}: { + base64Video: string; + width: number; + height: number; +}): Promise { + // Create a buffer from the base64 string, skipping the data URI scheme + const base64Data = base64Video.replace(/^data:video\/mp4;base64,/, ""); + const videoBuffer = Buffer.from(base64Data, "base64"); + + // Create a temporary file for the input video + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-crop-input-")); + const inputVideoPath = path.join(tempDir, `input.mp4`); + await fs.writeFile(inputVideoPath, videoBuffer); + + // Create a temporary file for the output video + const outputTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-crop-output-")); + const outputVideoPath = path.join(outputTempDir, `output-cropped.mp4`); + + // Return a promise that resolves with the path to the output cropped video file + return new Promise((resolve, reject) => { + ffmpeg(inputVideoPath) + .ffprobe((err, metadata) => { + if (err) { + reject(new Error(`Error reading video metadata: ${err.message}`)); + return; + } + + const videoStream = metadata.streams.find(s => s.codec_type === "video"); + if (!videoStream) { + reject(new Error(`Cannot find video stream in file: ${inputVideoPath}`)); + return; + } + + const { width: inWidth, height: inHeight } = videoStream; + const x = Math.floor((inWidth - width) / 2); + const y = Math.floor((inHeight - height) / 2); + + ffmpeg(inputVideoPath) + .outputOptions([ + `-vf crop=${width}:${height}:${x}:${y}` + ]) + .on("error", (err) => { + reject(new Error(`Error cropping video: ${err.message}`)); + }) + .on("end", () => { + resolve(outputVideoPath); + }) + .on('codecData', (data) => { + console.log('Input is ' + data.audio + ' audio ' + + 'with ' + data.video + ' video'); + }) + .save(outputVideoPath); + }); + }); +} \ No newline at end of file diff --git a/src/core/ffmpeg/cropVideo.mts b/src/core/ffmpeg/cropVideo.mts new file mode 100644 index 0000000000000000000000000000000000000000..68bb8026ebe44aaf9ada3e8b7132b8e14ac3fea4 --- /dev/null +++ b/src/core/ffmpeg/cropVideo.mts @@ -0,0 +1,76 @@ +import { promises as fs } from "node:fs"; +// import { writeFile, readFile } from 'node:fs/promises'; +import os from "node:os"; +import path from "node:path"; + +import ffmpeg from "fluent-ffmpeg"; + +export async function cropVideo({ + inputVideoPath, + width, + height, + debug = false, + asBase64 = false, +}: { + inputVideoPath: string + width: number + height: number + debug?: boolean + asBase64?: boolean +}): Promise { + // Verify that the input file exists + if (!(await fs.stat(inputVideoPath)).isFile()) { + throw new Error(`Input video file does not exist: ${inputVideoPath}`); + } + + // Create a temporary file for the output + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-crop-")); + const outputVideoPath = path.join(tempDir, `${path.parse(inputVideoPath).name}-cropped.mp4`); + + // Return a promise that resolves with the path to the output cropped video file + return new Promise((resolve, reject) => { + ffmpeg(inputVideoPath) + .ffprobe((err, metadata) => { + if (err) { + reject(new Error(`Error reading video metadata: ${err.message}`)); + return; + } + + const videoStream = metadata.streams.find(s => s.codec_type === "video"); + if (!videoStream) { + reject(new Error(`Cannot find video stream in file: ${inputVideoPath}`)); + return; + } + + const { width: inWidth, height: inHeight } = videoStream; + const x = Math.floor((inWidth - width) / 2); + const y = Math.floor((inHeight - height) / 2); + + ffmpeg(inputVideoPath) + .outputOptions([ + `-vf crop=${width}:${height}:${x}:${y}` + ]) + .on("error", (err) => { + reject(new Error(`Error cropping video: ${err.message}`)); + }) + .on("end", async () => { + if (!asBase64) { + resolve(outputVideoPath) + return + } + // Convert the output file to a base64 string + try { + const videoBuffer = await fs.readFile(outputVideoPath); + const videoBase64 = `data:video/mp4;base64,${videoBuffer.toString('base64')}`; + resolve(videoBase64); + } catch (error) { + reject(new Error(`Error loading the video file: ${error}`)); + } finally { + // Clean up temporary files + await fs.rm(tempDir, { recursive: true }); + } + }) + .save(outputVideoPath); + }); + }); +} \ No newline at end of file diff --git a/src/core/ffmpeg/getMediaInfo.mts b/src/core/ffmpeg/getMediaInfo.mts new file mode 100644 index 0000000000000000000000000000000000000000..3c3985c65753f440fbb2423aa02ce41dd8f5b588 --- /dev/null +++ b/src/core/ffmpeg/getMediaInfo.mts @@ -0,0 +1,79 @@ +import ffmpeg from "fluent-ffmpeg"; + +import { tmpdir } from "node:os"; +import { promises as fs } from "node:fs"; +import { join } from "node:path"; + +export type MediaMetadata = { + durationInSec: number; + durationInMs: number; + hasAudio: boolean; +}; + +/** + * Get the media info of a base64 or file path + * @param input + * @returns + */ +export async function getMediaInfo(input: string): Promise { + // If the input is a base64 string + if (input.startsWith("data:")) { + // Extract the base64 content + const base64Content = input.split(";base64,").pop(); + if (!base64Content) { + throw new Error("Invalid base64 data"); + } + + // Decode the base64 content to a buffer + const buffer = Buffer.from(base64Content, 'base64'); + + // Generate a temporary file name + const tempFileName = join(tmpdir(), `temp-media-${Date.now()}`); + + // Write the buffer to a temporary file + await fs.writeFile(tempFileName, buffer); + + // Get metadata from the temporary file then delete the file + try { + return await getMetaDataFromPath(tempFileName); + } finally { + await fs.unlink(tempFileName); + } + } + + // If the input is a path to the file + return await getMetaDataFromPath(input); +} + +async function getMetaDataFromPath(filePath: string): Promise { + return new Promise((resolve, reject) => { + ffmpeg.ffprobe(filePath, (err, metadata) => { + + let results = { + durationInSec: 0, + durationInMs: 0, + hasAudio: false, + } + + if (err) { + console.error("getMediaInfo(): failed to analyze the source (might happen with empty files)") + // reject(err); + resolve(results); + return; + } + + try { + results.durationInSec = metadata?.format?.duration || 0; + results.durationInMs = results.durationInSec * 1000; + results.hasAudio = (metadata?.streams || []).some((stream) => stream.codec_type === 'audio'); + + } catch (err) { + console.error(`getMediaInfo(): failed to analyze the source (might happen with empty files)`) + results.durationInSec = 0 + results.durationInMs = 0 + results.hasAudio = false + } + resolve(results); + }); + }); +} \ No newline at end of file diff --git a/src/core/ffmpeg/scaleVideo.mts b/src/core/ffmpeg/scaleVideo.mts new file mode 100644 index 0000000000000000000000000000000000000000..70a4aa3eada2f917f8453588b00c56464ee43efc --- /dev/null +++ b/src/core/ffmpeg/scaleVideo.mts @@ -0,0 +1,90 @@ +import fs from 'node:fs/promises'; +import { writeFile, readFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { v4 as uuidv4 } from "uuid"; +import ffmpeg from 'fluent-ffmpeg'; + +export type ScaleVideoParams = { + input: string; + height: number; + debug?: boolean; + asBase64?: boolean; +} + +/** + * Rescale a video (either file or base 64) to a given height. + * This returns a base64 video. + * + * Some essential things to note in this implementation: + * + * If the input is a valid base64 string, it gets decoded and stored as a temporary .mp4 file. + * The ffmpeg.outputOptions includes the arguments for setting the output video height and keeping the aspect ratio intact. The -1 in scale=-1:${height} tells ffmpeg to preserve aspect ratio based on the height. + * The output is a libx264-encoded MP4 video, matching typical browser support standards. + * Upon completion, the temporary output file is read into a buffer, converted to a base64 string with the correct prefix, and then cleaned up by removing temporary files. + * To call this function with desired input and height, you'd use it similarly to the provided convertMp4ToMp3 function example, being mindful that input must be a file path or properly-formatted base64 string and height is a number representing the new height of the video. + * + * Enter your message... + * + * @param param0 + * @returns + */ +export async function scaleVideo({ + input, + height, + asBase64 = false, + debug = false +}: ScaleVideoParams): Promise { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-")); + const tempOutPath = path.join(tempDir, `${uuidv4()}.mp4`); + + let inputPath; + if (input.startsWith('data:')) { + // Extract the base64 content and decode it to a temporary file + const base64Content = input.split(';base64,').pop(); + if (!base64Content) { + throw new Error('Invalid base64 input provided'); + } + inputPath = path.join(tempDir, `${uuidv4()}.mp4`); + await writeFile(inputPath, base64Content, 'base64'); + } else { + inputPath = input; + } + + if (debug) { + console.log("inputPath:", inputPath) + } + + // Return a promise that resolves with the base64 string of the output video + return new Promise((resolve, reject) => { + ffmpeg(inputPath) + .outputOptions([ + '-vf', `scale=-1:${height}`, + '-c:v', 'libx264', + '-preset', 'fast', + '-crf', '22' + ]) + .on('error', (err) => { + reject(new Error(`Error scaling the video: ${err.message}`)); + }) + .on('end', async () => { + if (!asBase64) { + resolve(tempOutPath) + return + } + // Convert the output file to a base64 string + try { + const videoBuffer = await readFile(tempOutPath); + const videoBase64 = `data:video/mp4;base64,${videoBuffer.toString('base64')}`; + resolve(videoBase64); + } catch (error) { + reject(new Error(`Error loading the video file: ${error}`)); + } finally { + // Clean up temporary files + await fs.rm(tempDir, { recursive: true }); + } + }) + .save(tempOutPath); + }); +} \ No newline at end of file diff --git a/src/core/files/deleteFileWithName.mts b/src/core/files/deleteFileWithName.mts new file mode 100644 index 0000000000000000000000000000000000000000..29a5cc7dd38abb0960f43d6e4b5386013246417c --- /dev/null +++ b/src/core/files/deleteFileWithName.mts @@ -0,0 +1,17 @@ +import { promises as fs } from "node:fs" +import path from "node:path" + +export const deleteFilesWithName = async (dir: string, name: string, debug?: boolean) => { + for (const file of await fs.readdir(dir)) { + if (file.includes(name)) { + const filePath = path.join(dir, file) + try { + await fs.unlink(filePath) + } catch (err) { + if (debug) { + console.error(`failed to unlink file in ${filePath}: ${err}`) + } + } + } + } +} diff --git a/src/core/files/downloadFileAsBase64.mts b/src/core/files/downloadFileAsBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..526fb07e29f0982385a5fd4306b7df02b1f25667 --- /dev/null +++ b/src/core/files/downloadFileAsBase64.mts @@ -0,0 +1,27 @@ +import { lookup } from "mime-types" + +export const downloadFileAsBase64 = async (remoteUrl: string): Promise => { + // const controller = new AbortController() + + // download the file + const response = await fetch(remoteUrl, { + // signal: controller.signal + }) + + // get as Buffer + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + // convert it to base64 + const base64 = buffer.toString('base64') + + + const res = lookup(remoteUrl) + let contentType = res.toString() + if (typeof res === "boolean" && res === false) { + contentType = response.headers.get('content-type') + } + + const assetUrl = `data:${contentType};base64,${base64}` + return assetUrl +}; \ No newline at end of file diff --git a/src/core/files/readJpegFileToBase64.mts b/src/core/files/readJpegFileToBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..26be11ffe9831c3d857f509515f42b410c8f43ef --- /dev/null +++ b/src/core/files/readJpegFileToBase64.mts @@ -0,0 +1,18 @@ +import { promises as fs } from "fs" + +export async function readJpegFileToBase64(filePath: string): Promise { + try { + // Read the file's content as a Buffer + const fileBuffer = await fs.readFile(filePath); + + // Convert the buffer to a base64 string + const base64 = fileBuffer.toString('base64'); + + // Prefix the base64 string with the Data URI scheme for PNG images + return `data:image/jpeg;base64,${base64}`; + } catch (error) { + // Handle errors (e.g., file not found, no permissions, etc.) + console.error(error); + throw error; + } +} diff --git a/src/core/files/readMp3FileToBase64.mts b/src/core/files/readMp3FileToBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..994f47dbf77f3b5d4a307fb97c6242b1b6668530 --- /dev/null +++ b/src/core/files/readMp3FileToBase64.mts @@ -0,0 +1,18 @@ +import { promises as fs } from "fs" + +export async function readMp3FileToBase64(filePath: string): Promise { + try { + // Read the file's content as a Buffer + const fileBuffer = await fs.readFile(filePath); + + // Convert the buffer to a base64 string + const base64 = fileBuffer.toString('base64'); + + // Prefix the base64 string with the Data URI scheme for PNG images + return `data:audio/mp3;base64,${base64}`; + } catch (error) { + // Handle errors (e.g., file not found, no permissions, etc.) + console.error(error); + throw error; + } +} diff --git a/src/core/files/readMp4FileToBase64.mts b/src/core/files/readMp4FileToBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..b6b1f4c20d3584ed1f34beb6daa12314237120a5 --- /dev/null +++ b/src/core/files/readMp4FileToBase64.mts @@ -0,0 +1,18 @@ +import { promises as fs } from "fs" + +export async function readMp4FileToBase64(filePath: string): Promise { + try { + // Read the file's content as a Buffer + const fileBuffer = await fs.readFile(filePath); + + // Convert the buffer to a base64 string + const base64 = fileBuffer.toString('base64'); + + // Prefix the base64 string with the Data URI scheme for PNG images + return `data:video/mp4;base64,${base64}`; + } catch (error) { + // Handle errors (e.g., file not found, no permissions, etc.) + console.error(error); + throw error; + } +} diff --git a/src/core/files/readPlainText.mts b/src/core/files/readPlainText.mts new file mode 100644 index 0000000000000000000000000000000000000000..81642cd2ac183432b76bc3a36a214033c1fb5731 --- /dev/null +++ b/src/core/files/readPlainText.mts @@ -0,0 +1,13 @@ +import { promises as fs } from "fs" + +export async function readPlainText(filePath: string): Promise { + try { + const plainText = await fs.readFile(filePath, "utf-8"); + + return plainText; + } catch (error) { + // Handle errors (e.g., file not found, no permissions, etc.) + console.error(error); + throw error; + } +} diff --git a/src/core/files/readPngFileToBase64.mts b/src/core/files/readPngFileToBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..8ce9649ccf55bf717cd86e194c2f23c4daded351 --- /dev/null +++ b/src/core/files/readPngFileToBase64.mts @@ -0,0 +1,18 @@ +import { promises as fs } from "fs" + +export async function readPngFileToBase64(filePath: string): Promise { + try { + // Read the file's content as a Buffer + const fileBuffer = await fs.readFile(filePath); + + // Convert the buffer to a base64 string + const base64 = fileBuffer.toString('base64'); + + // Prefix the base64 string with the Data URI scheme for PNG images + return `data:image/png;base64,${base64}`; + } catch (error) { + // Handle errors (e.g., file not found, no permissions, etc.) + console.error(error); + throw error; + } +} diff --git a/src/core/files/readWavFileToBase64.mts b/src/core/files/readWavFileToBase64.mts new file mode 100644 index 0000000000000000000000000000000000000000..c9ba1281b6817e85f88da5efe2f424a2d11c4eda --- /dev/null +++ b/src/core/files/readWavFileToBase64.mts @@ -0,0 +1,18 @@ +import { promises as fs } from "fs" + +export async function readWavFileToBase64(filePath: string): Promise { + try { + // Read the file's content as a Buffer + const fileBuffer = await fs.readFile(filePath); + + // Convert the buffer to a base64 string + const base64 = fileBuffer.toString('base64'); + + // Prefix the base64 string with the Data URI scheme for PNG images + return `data:audio/wav;base64,${base64}`; + } catch (error) { + // Handle errors (e.g., file not found, no permissions, etc.) + console.error(error); + throw error; + } +} diff --git a/src/core/files/removeTmpFiles.mts b/src/core/files/removeTmpFiles.mts new file mode 100644 index 0000000000000000000000000000000000000000..d5e1002f55b7d2e08ef636458212085d5ee67bb7 --- /dev/null +++ b/src/core/files/removeTmpFiles.mts @@ -0,0 +1,25 @@ +import { existsSync, promises as fs } from "node:fs" + +import { keepTemporaryFiles } from "../config.mts" + +// note: this function will never fail +export async function removeTemporaryFiles(filesPaths: string[]) { + try { + if (!keepTemporaryFiles) { + // Cleanup temporary files - you could choose to do this or leave it to the user + await Promise.all(filesPaths.map(async (filePath) => { + try { + if (existsSync(filePath)) { + await fs.unlink(filePath) + } + } catch (err) { + // + } + })) + } + } catch (err) { + // no big deal, except a bit of tmp file leak + // although.. if delete failed, it could also indicate + // that the file has already been cleaned-up, so even better! + } +} \ No newline at end of file diff --git a/src/core/files/resizeImage.mts b/src/core/files/resizeImage.mts new file mode 100644 index 0000000000000000000000000000000000000000..5a1b53f2a79abd0f523dbe7fd69c1fc1fbccfb62 --- /dev/null +++ b/src/core/files/resizeImage.mts @@ -0,0 +1,55 @@ +import sharp from "sharp"; + +export type ResizeImageParams = { + input: string + width?: number + height?: number + debug?: boolean + asBase64?: boolean // TODO: not implemented yet! +}; + +/** + * Resize an image to a given width and height. + * The input image can be a file path or a data URI (base64) + * The image ratio will be preserved if only one side is given. + * The image format (WebP, Jpeg, PNG) will be preserved. + * This function always return a base64 string (data URI with the mime type) + * + * @param param0 + * @returns + */ +export async function resizeImage({ input, width, height, debug, asBase64 }: ResizeImageParams): Promise { + let inputBuffer: Buffer; + + // Test if input is a data URI + const dataUriPattern = /^data:([a-zA-Z]+\/[a-zA-Z]+);base64,(.*)$/; + const matches = input.match(dataUriPattern); + + if (matches) { + const [, mimeType, base64Data] = matches; + if (!/^image\/(png|jpeg|webp)$/.test(mimeType)) { + throw new Error(`Unsupported image format. Expected PNG, JPEG, or WebP.`); + } + inputBuffer = Buffer.from(base64Data, "base64"); + } else { + // Assuming input is a file path + inputBuffer = await sharp(input).toBuffer(); + } + + const sharpInstance = sharp(inputBuffer) + .resize(width, height, { + fit: "inside", + withoutEnlargement: true + }); + + const outputBuffer = await sharpInstance.toBuffer(); + const outputMimeType = await sharpInstance.metadata().then(meta => meta.format); + + if (!outputMimeType) { + throw new Error("Failed to determine the image mime type after resizing."); + } + + const prefix = `data:image/${outputMimeType};base64,`; + const outputBase64 = outputBuffer.toString("base64"); + return `${prefix}${outputBase64}`; +} \ No newline at end of file diff --git a/src/core/files/writeBase64ToFile.mts b/src/core/files/writeBase64ToFile.mts new file mode 100644 index 0000000000000000000000000000000000000000..27149a54662a2044dfa49ba906786b857b9f6dc0 --- /dev/null +++ b/src/core/files/writeBase64ToFile.mts @@ -0,0 +1,29 @@ +import { promises as fs } from "node:fs" + +export async function writeBase64ToFile(base64Data: string, filePath: string): Promise { + const data = base64Data.split(";base64,").pop() + if (!data) { throw new Error("Invalid base64 content") } + await fs.writeFile(filePath, data, { encoding: "base64" }) + return filePath +} + +// legacy way: with more manual steps + +/* +export async function writeBase64ToFile(content: string, filePath: string): Promise { + + // Remove "data:image/png;base64," from the start of the data url + const base64Data = content.split(";base64,")[1] + + // Convert base64 to binary + const data = Buffer.from(base64Data, "base64") + + // Write binary data to file + try { + await fs.writeFile(filePath, data) + // console.log("File written successfully") + } catch (error) { + console.error("An error occurred:", error) + } +} +*/ \ No newline at end of file diff --git a/src/core/parsers/getValidNumber.mts b/src/core/parsers/getValidNumber.mts new file mode 100644 index 0000000000000000000000000000000000000000..841fc4afab03b33fbc6e6349b41d9099a3073cb3 --- /dev/null +++ b/src/core/parsers/getValidNumber.mts @@ -0,0 +1,10 @@ +export const getValidNumber = (something: any, minValue: number, maxValue: number, defaultValue: number) => { + const strValue = `${something || defaultValue}` + const numValue = Number(strValue) + const isValid = !isNaN(numValue) && isFinite(numValue) + if (!isValid) { + return defaultValue + } + return Math.max(minValue, Math.min(maxValue, numValue)) + +} \ No newline at end of file diff --git a/src/core/parsers/parseArray.mts b/src/core/parsers/parseArray.mts new file mode 100644 index 0000000000000000000000000000000000000000..0ea4e0ad11f857c0c69d3ff5cc3b238b8f544498 --- /dev/null +++ b/src/core/parsers/parseArray.mts @@ -0,0 +1,10 @@ +export function parseArray(text: string) { + let array: string[] = [] + try { + array = JSON.parse(text) + } catch (err) { + array = [] + } + + return array +} \ No newline at end of file diff --git a/src/core/utils/formatProgress.mts b/src/core/utils/formatProgress.mts new file mode 100644 index 0000000000000000000000000000000000000000..52abaf368adc202c1e2e9c661cba19811edb1067 --- /dev/null +++ b/src/core/utils/formatProgress.mts @@ -0,0 +1,3 @@ +export function formatProgress(a: number, b: number) { + return `${Math.round((a / b) * 100)}%` +} \ No newline at end of file diff --git a/src/core/utils/getCssStyle.mts b/src/core/utils/getCssStyle.mts new file mode 100644 index 0000000000000000000000000000000000000000..ae3c1f94f2460cbb4ffa14fb386494836a01a21e --- /dev/null +++ b/src/core/utils/getCssStyle.mts @@ -0,0 +1,94 @@ + +export type TextOverlayStyle = + | "outline" + | "highlight" + +export type TextOverlayFont = + | "Montserrat" + | "Sofia" + +export type TextOverlayFontWeight = + | 100 + | 200 + | 300 + | 400 + | 500 + | 600 + | 700 + | 800 + | 900 + +export function getCssStyle({ + width, + height, + fontSize, + fontFamily, + fontWeight, +}: { + width?: number | string + height?: number | string + fontSize: number + fontFamily: TextOverlayFont + fontWeight: TextOverlayFontWeight +}) { + + return ` + + + + + ` +} \ No newline at end of file diff --git a/src/core/utils/isValidNumber.mts b/src/core/utils/isValidNumber.mts new file mode 100644 index 0000000000000000000000000000000000000000..a4e24e3aae75cd7851509aa8533cad1858b39792 --- /dev/null +++ b/src/core/utils/isValidNumber.mts @@ -0,0 +1,7 @@ +export function isValidNumber(input: any) { + return ( + typeof (input) === "number" && + isFinite(input) && + !isNaN(input) + ) +} \ No newline at end of file diff --git a/src/core/utils/sleep.mts b/src/core/utils/sleep.mts new file mode 100644 index 0000000000000000000000000000000000000000..2885c6e75c0dc415c9eaf71beabac7461eee5588 --- /dev/null +++ b/src/core/utils/sleep.mts @@ -0,0 +1,6 @@ +export const sleep = async (durationInMs: number) => + new Promise((resolve) => { + setTimeout(() => { + resolve(true) + }, durationInMs) + }) \ No newline at end of file diff --git a/src/core/utils/timeout.mts b/src/core/utils/timeout.mts new file mode 100644 index 0000000000000000000000000000000000000000..7ab21d734c0af4d1138b1f0cdefb158c46631110 --- /dev/null +++ b/src/core/utils/timeout.mts @@ -0,0 +1,15 @@ +export function timeout( + promise: Promise, + ms: number, + timeoutError = new Error('Promise timed out') +): Promise { + // create a promise that rejects in milliseconds + const promiseWithTimeout = new Promise((_, reject) => { + setTimeout(() => { + reject(timeoutError); + }, ms); + }); + + // returns a race between timeout and the passed promise + return Promise.race([promise, promiseWithTimeout]); +} \ No newline at end of file diff --git a/src/index.mts b/src/index.mts new file mode 100644 index 0000000000000000000000000000000000000000..42ee13f233481a96b7fff4d1b74817b64eb135de --- /dev/null +++ b/src/index.mts @@ -0,0 +1,58 @@ + +import express from "express" +import { Blob } from "buffer" + +import { parseClap } from "./core/clap/parseClap.mts" +import { ClapProject } from "./core/clap/types.mts" + +const app = express() +const port = 7860 + +process.on('unhandledRejection', (reason: string, p: Promise) => { + console.error('Unhandled Rejection at:', p, 'reason:', reason); +}) + +process.on('uncaughtException', (error: Error) => { + console.error(`Caught exception: ${error}\n` + `Exception origin: ${error.stack}`); +}) + +// fix this error: "PayloadTooLargeError: request entity too large" +// there are multiple version because.. yeah well, it's Express! +// app.use(bodyParser.json({limit: '50mb'})); +//app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); +app.use(express.json({ limit: '200mb' })); +app.use(express.urlencoded({ limit: '200mb', extended: true })); + +// the export robot has only one job: to export .clap files +app.post("/export", async (req, res) => { + + let data: Uint8Array[] = []; + + req.on("data", (chunk) => { + data.push(chunk); + }); + + req.on("end", async () => { + let clapProject: ClapProject + try { + let fileData = Buffer.concat(data); + const clapBlob = new Blob([fileData]); + clapProject = await parseClap(clapBlob); + console.log("got a clap project!:", clapProject) + } catch (err) { + console.error(`failed to parse the request: ${err}`) + res.status(500) + res.write(JSON.stringify({ "error": `${err}` })) + res.end() + return + } + // TODO read the mp4 file and convert it to + res.status(200) + res.write("TODO") + res.end() + }); +}) + +app.listen(port, () => { + console.log(`Open http://localhost:${port}`) +}) \ No newline at end of file diff --git a/src/main.mts b/src/main.mts new file mode 100644 index 0000000000000000000000000000000000000000..cf25aed91ad5e5b69216a2250be31129eedf5238 --- /dev/null +++ b/src/main.mts @@ -0,0 +1,71 @@ +import { tmpdir } from "node:os" +import { join } from "node:path" +import { mkdtemp } from "node:fs/promises" +import { v4 as uuidv4 } from "uuid" + +import { ClapProject } from "./core/clap/types.mts"; +import { concatenateAudio } from "./core/ffmpeg/concatenateAudio.mts"; +import { concatenateVideosWithAudio } from "./core/ffmpeg/concatenateVideosWithAudio.mts"; +import { writeBase64ToFile } from "./core/files/writeBase64ToFile.mts"; +import { concatenateVideos } from "./core/ffmpeg/concatenateVideos.mts" +import { deleteFilesWithName } from "./core/files/deleteFileWithName.mts" + +/** + * Generate a .mp4 video inside a direcory (if none is provided, it will be created in /tmp) + * + * @param clap + * @returns file path to the final .mp4 + */ +export async function clapToTmpVideoFilePath(clap: ClapProject, dir = ""): Promise { + + dir = dir || (await mkdtemp(join(tmpdir(), uuidv4()))) + + const videoFilePaths: string[] = [] + const videoSegments = clap.segments.filter(s => s.category === "video" && s.assetUrl.startsWith("data:video/")) + + for (const segment of videoSegments) { + videoFilePaths.push( + await writeBase64ToFile( + segment.assetUrl, + join(dir, `tmp_asset_${segment.id}.mp4`) + ) + ) + } + + const concatenatedVideosNoSound = await concatenateVideos({ + videoFilePaths, + output: join(dir, `tmp_asset_concatenated_videos.mp4`) + }) + + const audioTracks: string[] = [] + + const musicSegments = clap.segments.filter(s => s.category === "music" && s.assetUrl.startsWith("data:audio/")) + for (const segment of musicSegments) { + audioTracks.push( + await writeBase64ToFile( + segment.assetUrl, + join(dir, `tmp_asset_${segment.id}.wav`) + ) + ) + } + + const concatenatedAudio = await concatenateAudio({ + output: join(dir, `tmp_asset_concatenated_audio.wav`), + audioTracks, + crossfadeDurationInSec: 2 // 2 seconds + }) + + const finalFilePathOfVideoWithSound = await concatenateVideosWithAudio({ + output: join(dir, `final_video.mp4`), + audioFilePath: concatenatedAudio.filepath, + videoFilePaths: [concatenatedVideosNoSound.filepath], + // videos are silent, so they can stay at 0 + videoTracksVolume: 0.0, + audioTrackVolume: 1.0, + }) + + // we delete all the temporary assets + await deleteFilesWithName(dir, `tmp_asset_`) + + return finalFilePathOfVideoWithSound +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..26d982ff3736ba8ddba75d56da6d2b75d26606c7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "module": "nodenext", + "noEmit": true, + "allowImportingTsExtensions": true, + "target": "es2022" + }, + "include": ["**/*.ts", "**/*.mts"], +} \ No newline at end of file