Spaces:
Running
Running
Commit ·
f357c45
1
Parent(s): 08ff55b
Add playwrigt vide render
Browse files- Dockerfile.build.lean +5 -0
- package-lock.json +188 -0
- package.json +1 -0
- utils/AvatarRender.js +1 -1
- utils/HtmlRender.js +24 -13
- utils/bubble/Bubble.js +14 -13
Dockerfile.build.lean
CHANGED
|
@@ -84,6 +84,7 @@ RUN node -e "console.log('Node version:', process.version)" && \
|
|
| 84 |
node -e "require('remotion'); console.log('Remotion OK')" && \
|
| 85 |
ls -la node_modules/.bin/ | head -20
|
| 86 |
|
|
|
|
| 87 |
# -----------------------------------------------------------------------------
|
| 88 |
# final production image
|
| 89 |
# -----------------------------------------------------------------------------
|
|
@@ -100,13 +101,17 @@ WORKDIR /app
|
|
| 100 |
COPY --chown=1000 . ./
|
| 101 |
COPY --chown=1000 start.sh /app/start.sh
|
| 102 |
RUN chmod +x /app/start.sh
|
|
|
|
|
|
|
| 103 |
RUN npm i
|
|
|
|
| 104 |
|
| 105 |
RUN echo "Verifying installation..." && \
|
| 106 |
node -e "console.log('Final verification - Node:', process.version)" && \
|
| 107 |
npm list --depth=0 | head -10 || true
|
| 108 |
|
| 109 |
EXPOSE 7860 3000 8083
|
|
|
|
| 110 |
CMD ["sh", "/app/start.sh"]
|
| 111 |
|
| 112 |
# build: docker build -f Dockerfile.build.lean -t semibit/render-farm:lean .
|
|
|
|
| 84 |
node -e "require('remotion'); console.log('Remotion OK')" && \
|
| 85 |
ls -la node_modules/.bin/ | head -20
|
| 86 |
|
| 87 |
+
|
| 88 |
# -----------------------------------------------------------------------------
|
| 89 |
# final production image
|
| 90 |
# -----------------------------------------------------------------------------
|
|
|
|
| 101 |
COPY --chown=1000 . ./
|
| 102 |
COPY --chown=1000 start.sh /app/start.sh
|
| 103 |
RUN chmod +x /app/start.sh
|
| 104 |
+
|
| 105 |
+
RUN npm install playwright
|
| 106 |
RUN npm i
|
| 107 |
+
RUN npx playwright install
|
| 108 |
|
| 109 |
RUN echo "Verifying installation..." && \
|
| 110 |
node -e "console.log('Final verification - Node:', process.version)" && \
|
| 111 |
npm list --depth=0 | head -10 || true
|
| 112 |
|
| 113 |
EXPOSE 7860 3000 8083
|
| 114 |
+
|
| 115 |
CMD ["sh", "/app/start.sh"]
|
| 116 |
|
| 117 |
# build: docker build -f Dockerfile.build.lean -t semibit/render-farm:lean .
|
package-lock.json
CHANGED
|
@@ -44,6 +44,7 @@
|
|
| 44 |
"path": "^0.12.7",
|
| 45 |
"pdfkit": "^0.15.0",
|
| 46 |
"playwright": "^1.60.0",
|
|
|
|
| 47 |
"puppeteer": "^21.3.6",
|
| 48 |
"react": "^18.0.0",
|
| 49 |
"react-dom": "^18.0.0",
|
|
@@ -2757,6 +2758,50 @@
|
|
| 2757 |
"postcss-selector-parser": "^6.0.10"
|
| 2758 |
}
|
| 2759 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2760 |
"node_modules/@emnapi/runtime": {
|
| 2761 |
"version": "1.3.1",
|
| 2762 |
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
|
@@ -7921,6 +7966,15 @@
|
|
| 7921 |
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 7922 |
}
|
| 7923 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7924 |
"node_modules/error-ex": {
|
| 7925 |
"version": "1.3.2",
|
| 7926 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
|
@@ -8779,6 +8833,22 @@
|
|
| 8779 |
"node": ">= 4.0.0"
|
| 8780 |
}
|
| 8781 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8782 |
"node_modules/file-entry-cache": {
|
| 8783 |
"version": "8.0.0",
|
| 8784 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
|
@@ -9712,6 +9782,21 @@
|
|
| 9712 |
"node": ">= 6"
|
| 9713 |
}
|
| 9714 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9715 |
"node_modules/http-signature": {
|
| 9716 |
"version": "1.2.0",
|
| 9717 |
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
|
@@ -14395,6 +14480,11 @@
|
|
| 14395 |
"node": ">=6"
|
| 14396 |
}
|
| 14397 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14398 |
"node_modules/parse-json": {
|
| 14399 |
"version": "5.2.0",
|
| 14400 |
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
|
@@ -14588,6 +14678,26 @@
|
|
| 14588 |
"node": ">=18"
|
| 14589 |
}
|
| 14590 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14591 |
"node_modules/pluralize": {
|
| 14592 |
"version": "8.0.0",
|
| 14593 |
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
|
@@ -19760,6 +19870,40 @@
|
|
| 19760 |
"integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==",
|
| 19761 |
"requires": {}
|
| 19762 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19763 |
"@emnapi/runtime": {
|
| 19764 |
"version": "1.3.1",
|
| 19765 |
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
|
@@ -23484,6 +23628,11 @@
|
|
| 23484 |
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
| 23485 |
"optional": true
|
| 23486 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23487 |
"error-ex": {
|
| 23488 |
"version": "1.3.2",
|
| 23489 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
|
@@ -24124,6 +24273,17 @@
|
|
| 24124 |
}
|
| 24125 |
}
|
| 24126 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24127 |
"file-entry-cache": {
|
| 24128 |
"version": "8.0.0",
|
| 24129 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
|
@@ -24815,6 +24975,21 @@
|
|
| 24815 |
"debug": "4"
|
| 24816 |
}
|
| 24817 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24818 |
"http-signature": {
|
| 24819 |
"version": "1.2.0",
|
| 24820 |
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
|
@@ -27984,6 +28159,11 @@
|
|
| 27984 |
"callsites": "^3.0.0"
|
| 27985 |
}
|
| 27986 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27987 |
"parse-json": {
|
| 27988 |
"version": "5.2.0",
|
| 27989 |
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
|
@@ -28117,6 +28297,14 @@
|
|
| 28117 |
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
| 28118 |
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="
|
| 28119 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28120 |
"pluralize": {
|
| 28121 |
"version": "8.0.0",
|
| 28122 |
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
|
|
|
| 44 |
"path": "^0.12.7",
|
| 45 |
"pdfkit": "^0.15.0",
|
| 46 |
"playwright": "^1.60.0",
|
| 47 |
+
"playwright-recorder-plus": "^0.1.3",
|
| 48 |
"puppeteer": "^21.3.6",
|
| 49 |
"react": "^18.0.0",
|
| 50 |
"react-dom": "^18.0.0",
|
|
|
|
| 2758 |
"postcss-selector-parser": "^6.0.10"
|
| 2759 |
}
|
| 2760 |
},
|
| 2761 |
+
"node_modules/@derhuerst/http-basic": {
|
| 2762 |
+
"version": "8.2.4",
|
| 2763 |
+
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
|
| 2764 |
+
"integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==",
|
| 2765 |
+
"license": "MIT",
|
| 2766 |
+
"dependencies": {
|
| 2767 |
+
"caseless": "^0.12.0",
|
| 2768 |
+
"concat-stream": "^2.0.0",
|
| 2769 |
+
"http-response-object": "^3.0.1",
|
| 2770 |
+
"parse-cache-control": "^1.0.1"
|
| 2771 |
+
},
|
| 2772 |
+
"engines": {
|
| 2773 |
+
"node": ">=6.0.0"
|
| 2774 |
+
}
|
| 2775 |
+
},
|
| 2776 |
+
"node_modules/@derhuerst/http-basic/node_modules/concat-stream": {
|
| 2777 |
+
"version": "2.0.0",
|
| 2778 |
+
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
| 2779 |
+
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
| 2780 |
+
"engines": [
|
| 2781 |
+
"node >= 6.0"
|
| 2782 |
+
],
|
| 2783 |
+
"license": "MIT",
|
| 2784 |
+
"dependencies": {
|
| 2785 |
+
"buffer-from": "^1.0.0",
|
| 2786 |
+
"inherits": "^2.0.3",
|
| 2787 |
+
"readable-stream": "^3.0.2",
|
| 2788 |
+
"typedarray": "^0.0.6"
|
| 2789 |
+
}
|
| 2790 |
+
},
|
| 2791 |
+
"node_modules/@derhuerst/http-basic/node_modules/readable-stream": {
|
| 2792 |
+
"version": "3.6.2",
|
| 2793 |
+
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
| 2794 |
+
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
| 2795 |
+
"license": "MIT",
|
| 2796 |
+
"dependencies": {
|
| 2797 |
+
"inherits": "^2.0.3",
|
| 2798 |
+
"string_decoder": "^1.1.1",
|
| 2799 |
+
"util-deprecate": "^1.0.1"
|
| 2800 |
+
},
|
| 2801 |
+
"engines": {
|
| 2802 |
+
"node": ">= 6"
|
| 2803 |
+
}
|
| 2804 |
+
},
|
| 2805 |
"node_modules/@emnapi/runtime": {
|
| 2806 |
"version": "1.3.1",
|
| 2807 |
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
|
|
|
| 7966 |
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 7967 |
}
|
| 7968 |
},
|
| 7969 |
+
"node_modules/env-paths": {
|
| 7970 |
+
"version": "2.2.1",
|
| 7971 |
+
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
| 7972 |
+
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
| 7973 |
+
"license": "MIT",
|
| 7974 |
+
"engines": {
|
| 7975 |
+
"node": ">=6"
|
| 7976 |
+
}
|
| 7977 |
+
},
|
| 7978 |
"node_modules/error-ex": {
|
| 7979 |
"version": "1.3.2",
|
| 7980 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
|
|
|
| 8833 |
"node": ">= 4.0.0"
|
| 8834 |
}
|
| 8835 |
},
|
| 8836 |
+
"node_modules/ffmpeg-static": {
|
| 8837 |
+
"version": "5.3.0",
|
| 8838 |
+
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
| 8839 |
+
"integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
|
| 8840 |
+
"hasInstallScript": true,
|
| 8841 |
+
"license": "GPL-3.0-or-later",
|
| 8842 |
+
"dependencies": {
|
| 8843 |
+
"@derhuerst/http-basic": "^8.2.0",
|
| 8844 |
+
"env-paths": "^2.2.0",
|
| 8845 |
+
"https-proxy-agent": "^5.0.0",
|
| 8846 |
+
"progress": "^2.0.3"
|
| 8847 |
+
},
|
| 8848 |
+
"engines": {
|
| 8849 |
+
"node": ">=16"
|
| 8850 |
+
}
|
| 8851 |
+
},
|
| 8852 |
"node_modules/file-entry-cache": {
|
| 8853 |
"version": "8.0.0",
|
| 8854 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
|
|
|
| 9782 |
"node": ">= 6"
|
| 9783 |
}
|
| 9784 |
},
|
| 9785 |
+
"node_modules/http-response-object": {
|
| 9786 |
+
"version": "3.0.2",
|
| 9787 |
+
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
|
| 9788 |
+
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
|
| 9789 |
+
"license": "MIT",
|
| 9790 |
+
"dependencies": {
|
| 9791 |
+
"@types/node": "^10.0.3"
|
| 9792 |
+
}
|
| 9793 |
+
},
|
| 9794 |
+
"node_modules/http-response-object/node_modules/@types/node": {
|
| 9795 |
+
"version": "10.17.60",
|
| 9796 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
|
| 9797 |
+
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
|
| 9798 |
+
"license": "MIT"
|
| 9799 |
+
},
|
| 9800 |
"node_modules/http-signature": {
|
| 9801 |
"version": "1.2.0",
|
| 9802 |
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
|
|
|
| 14480 |
"node": ">=6"
|
| 14481 |
}
|
| 14482 |
},
|
| 14483 |
+
"node_modules/parse-cache-control": {
|
| 14484 |
+
"version": "1.0.1",
|
| 14485 |
+
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
| 14486 |
+
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
|
| 14487 |
+
},
|
| 14488 |
"node_modules/parse-json": {
|
| 14489 |
"version": "5.2.0",
|
| 14490 |
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
|
|
|
| 14678 |
"node": ">=18"
|
| 14679 |
}
|
| 14680 |
},
|
| 14681 |
+
"node_modules/playwright-recorder-plus": {
|
| 14682 |
+
"version": "0.1.3",
|
| 14683 |
+
"resolved": "https://registry.npmjs.org/playwright-recorder-plus/-/playwright-recorder-plus-0.1.3.tgz",
|
| 14684 |
+
"integrity": "sha512-s78vcWbWTiKLGE0omxQKG2uKYRf+wUWU7CU6hAw+Tgt637KGsd60+KAZNILHRILtMdy2B8GqLZaNfOB29cSmVg==",
|
| 14685 |
+
"license": "MIT",
|
| 14686 |
+
"dependencies": {
|
| 14687 |
+
"ffmpeg-static": "^5.3.0"
|
| 14688 |
+
},
|
| 14689 |
+
"engines": {
|
| 14690 |
+
"node": ">=18"
|
| 14691 |
+
},
|
| 14692 |
+
"peerDependencies": {
|
| 14693 |
+
"playwright-core": "^1.59.0"
|
| 14694 |
+
},
|
| 14695 |
+
"peerDependenciesMeta": {
|
| 14696 |
+
"playwright-core": {
|
| 14697 |
+
"optional": false
|
| 14698 |
+
}
|
| 14699 |
+
}
|
| 14700 |
+
},
|
| 14701 |
"node_modules/pluralize": {
|
| 14702 |
"version": "8.0.0",
|
| 14703 |
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
|
|
|
| 19870 |
"integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==",
|
| 19871 |
"requires": {}
|
| 19872 |
},
|
| 19873 |
+
"@derhuerst/http-basic": {
|
| 19874 |
+
"version": "8.2.4",
|
| 19875 |
+
"resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz",
|
| 19876 |
+
"integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==",
|
| 19877 |
+
"requires": {
|
| 19878 |
+
"caseless": "^0.12.0",
|
| 19879 |
+
"concat-stream": "^2.0.0",
|
| 19880 |
+
"http-response-object": "^3.0.1",
|
| 19881 |
+
"parse-cache-control": "^1.0.1"
|
| 19882 |
+
},
|
| 19883 |
+
"dependencies": {
|
| 19884 |
+
"concat-stream": {
|
| 19885 |
+
"version": "2.0.0",
|
| 19886 |
+
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
| 19887 |
+
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
| 19888 |
+
"requires": {
|
| 19889 |
+
"buffer-from": "^1.0.0",
|
| 19890 |
+
"inherits": "^2.0.3",
|
| 19891 |
+
"readable-stream": "^3.0.2",
|
| 19892 |
+
"typedarray": "^0.0.6"
|
| 19893 |
+
}
|
| 19894 |
+
},
|
| 19895 |
+
"readable-stream": {
|
| 19896 |
+
"version": "3.6.2",
|
| 19897 |
+
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
| 19898 |
+
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
| 19899 |
+
"requires": {
|
| 19900 |
+
"inherits": "^2.0.3",
|
| 19901 |
+
"string_decoder": "^1.1.1",
|
| 19902 |
+
"util-deprecate": "^1.0.1"
|
| 19903 |
+
}
|
| 19904 |
+
}
|
| 19905 |
+
}
|
| 19906 |
+
},
|
| 19907 |
"@emnapi/runtime": {
|
| 19908 |
"version": "1.3.1",
|
| 19909 |
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
|
|
|
| 23628 |
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
| 23629 |
"optional": true
|
| 23630 |
},
|
| 23631 |
+
"env-paths": {
|
| 23632 |
+
"version": "2.2.1",
|
| 23633 |
+
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
| 23634 |
+
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="
|
| 23635 |
+
},
|
| 23636 |
"error-ex": {
|
| 23637 |
"version": "1.3.2",
|
| 23638 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
|
|
|
| 24273 |
}
|
| 24274 |
}
|
| 24275 |
},
|
| 24276 |
+
"ffmpeg-static": {
|
| 24277 |
+
"version": "5.3.0",
|
| 24278 |
+
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
| 24279 |
+
"integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==",
|
| 24280 |
+
"requires": {
|
| 24281 |
+
"@derhuerst/http-basic": "^8.2.0",
|
| 24282 |
+
"env-paths": "^2.2.0",
|
| 24283 |
+
"https-proxy-agent": "^5.0.0",
|
| 24284 |
+
"progress": "^2.0.3"
|
| 24285 |
+
}
|
| 24286 |
+
},
|
| 24287 |
"file-entry-cache": {
|
| 24288 |
"version": "8.0.0",
|
| 24289 |
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
|
|
|
| 24975 |
"debug": "4"
|
| 24976 |
}
|
| 24977 |
},
|
| 24978 |
+
"http-response-object": {
|
| 24979 |
+
"version": "3.0.2",
|
| 24980 |
+
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
|
| 24981 |
+
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
|
| 24982 |
+
"requires": {
|
| 24983 |
+
"@types/node": "^10.0.3"
|
| 24984 |
+
},
|
| 24985 |
+
"dependencies": {
|
| 24986 |
+
"@types/node": {
|
| 24987 |
+
"version": "10.17.60",
|
| 24988 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
|
| 24989 |
+
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
|
| 24990 |
+
}
|
| 24991 |
+
}
|
| 24992 |
+
},
|
| 24993 |
"http-signature": {
|
| 24994 |
"version": "1.2.0",
|
| 24995 |
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
|
|
|
| 28159 |
"callsites": "^3.0.0"
|
| 28160 |
}
|
| 28161 |
},
|
| 28162 |
+
"parse-cache-control": {
|
| 28163 |
+
"version": "1.0.1",
|
| 28164 |
+
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
| 28165 |
+
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
|
| 28166 |
+
},
|
| 28167 |
"parse-json": {
|
| 28168 |
"version": "5.2.0",
|
| 28169 |
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
|
|
|
| 28297 |
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
| 28298 |
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="
|
| 28299 |
},
|
| 28300 |
+
"playwright-recorder-plus": {
|
| 28301 |
+
"version": "0.1.3",
|
| 28302 |
+
"resolved": "https://registry.npmjs.org/playwright-recorder-plus/-/playwright-recorder-plus-0.1.3.tgz",
|
| 28303 |
+
"integrity": "sha512-s78vcWbWTiKLGE0omxQKG2uKYRf+wUWU7CU6hAw+Tgt637KGsd60+KAZNILHRILtMdy2B8GqLZaNfOB29cSmVg==",
|
| 28304 |
+
"requires": {
|
| 28305 |
+
"ffmpeg-static": "^5.3.0"
|
| 28306 |
+
}
|
| 28307 |
+
},
|
| 28308 |
"pluralize": {
|
| 28309 |
"version": "8.0.0",
|
| 28310 |
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
package.json
CHANGED
|
@@ -84,6 +84,7 @@
|
|
| 84 |
"path": "^0.12.7",
|
| 85 |
"pdfkit": "^0.15.0",
|
| 86 |
"playwright": "^1.60.0",
|
|
|
|
| 87 |
"puppeteer": "^21.3.6",
|
| 88 |
"react": "^18.0.0",
|
| 89 |
"react-dom": "^18.0.0",
|
|
|
|
| 84 |
"path": "^0.12.7",
|
| 85 |
"pdfkit": "^0.15.0",
|
| 86 |
"playwright": "^1.60.0",
|
| 87 |
+
"playwright-recorder-plus": "^0.1.3",
|
| 88 |
"puppeteer": "^21.3.6",
|
| 89 |
"react": "^18.0.0",
|
| 90 |
"react-dom": "^18.0.0",
|
utils/AvatarRender.js
CHANGED
|
@@ -85,7 +85,7 @@ export class AvatarRenderer {
|
|
| 85 |
const bubbledFile = path.join(outDir, `${jobId}_section${i}_bubbled.mp4`);
|
| 86 |
try {
|
| 87 |
onLog && onLog(`Applying ${section.bubbles.length} bubble(s) to section ${i}...\n`);
|
| 88 |
-
await Bubble.makeBubble(sectionVideo, section.bubbles, bubbledFile, onLog);
|
| 89 |
tempFiles.push(bubbledFile);
|
| 90 |
sourceAfterBubbles = bubbledFile;
|
| 91 |
} catch (e) {
|
|
|
|
| 85 |
const bubbledFile = path.join(outDir, `${jobId}_section${i}_bubbled.mp4`);
|
| 86 |
try {
|
| 87 |
onLog && onLog(`Applying ${section.bubbles.length} bubble(s) to section ${i}...\n`);
|
| 88 |
+
await Bubble.makeBubble(sectionVideo, section.bubbles, bubbledFile, onLog, undefined, undefined, originalManuscript.meta);
|
| 89 |
tempFiles.push(bubbledFile);
|
| 90 |
sourceAfterBubbles = bubbledFile;
|
| 91 |
} catch (e) {
|
utils/HtmlRender.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { chromium } from 'playwright';
|
|
|
|
| 2 |
import fs from 'fs';
|
| 3 |
import path from 'path';
|
| 4 |
|
|
@@ -33,30 +34,40 @@ export class HtmlRender {
|
|
| 33 |
if (!this.browser) {
|
| 34 |
this.browser = await chromium.launch();
|
| 35 |
}
|
| 36 |
-
const context = await this.browser.newContext(
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
});
|
| 42 |
-
const page = await context.newPage();
|
| 43 |
-
if (options.viewport) await page.setViewportSize(options.viewport);
|
| 44 |
|
| 45 |
if (html) {
|
| 46 |
await page.setContent(html, { waitUntil: 'networkidle' });
|
| 47 |
} else if (htmlFile) {
|
| 48 |
-
|
|
|
|
| 49 |
}
|
| 50 |
|
|
|
|
| 51 |
await page.waitForTimeout(durationSec * 1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
await page.close();
|
| 53 |
await context.close();
|
| 54 |
-
|
| 55 |
-
const videoPath = await page.video().path();
|
| 56 |
-
if (fs.existsSync(videoPath)) {
|
| 57 |
-
if (fs.existsSync(outFilePath)) fs.unlinkSync(outFilePath);
|
| 58 |
-
fs.renameSync(videoPath, outFilePath);
|
| 59 |
-
}
|
| 60 |
}
|
| 61 |
|
| 62 |
async close() {
|
|
|
|
| 1 |
import { chromium } from 'playwright';
|
| 2 |
+
import { attachRecorder } from 'playwright-recorder-plus';
|
| 3 |
import fs from 'fs';
|
| 4 |
import path from 'path';
|
| 5 |
|
|
|
|
| 34 |
if (!this.browser) {
|
| 35 |
this.browser = await chromium.launch();
|
| 36 |
}
|
| 37 |
+
const context = await this.browser.newContext();
|
| 38 |
+
const page = await context.newPage();
|
| 39 |
+
|
| 40 |
+
const vw = (options.viewport && options.viewport.width) || 1080;
|
| 41 |
+
const vh = (options.viewport && options.viewport.height) || 1920;
|
| 42 |
+
await page.setViewportSize({ width: vw, height: vh });
|
| 43 |
+
|
| 44 |
+
// Attach high-quality recorder
|
| 45 |
+
const recorder = await attachRecorder(page, {
|
| 46 |
+
path: outFilePath,
|
| 47 |
+
fps: options.fps || 30,
|
| 48 |
+
ffmpegOptions: {
|
| 49 |
+
// Ensuring high quality for the render farm
|
| 50 |
+
crf: 18,
|
| 51 |
+
preset: 'veryfast'
|
| 52 |
}
|
| 53 |
});
|
|
|
|
|
|
|
| 54 |
|
| 55 |
if (html) {
|
| 56 |
await page.setContent(html, { waitUntil: 'networkidle' });
|
| 57 |
} else if (htmlFile) {
|
| 58 |
+
const absPath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
|
| 59 |
+
await page.goto(`file://${absPath}`, { waitUntil: 'networkidle' });
|
| 60 |
}
|
| 61 |
|
| 62 |
+
// Wait for the requested duration
|
| 63 |
await page.waitForTimeout(durationSec * 1000);
|
| 64 |
+
|
| 65 |
+
// Stop and finalize
|
| 66 |
+
await recorder.stop();
|
| 67 |
+
await recorder.finalized;
|
| 68 |
+
|
| 69 |
await page.close();
|
| 70 |
await context.close();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
async close() {
|
utils/bubble/Bubble.js
CHANGED
|
@@ -159,7 +159,7 @@ class BubbleMaker {
|
|
| 159 |
* @param {boolean} detachAudio - kept for API compatibility; audio is always handled separately now
|
| 160 |
* @param {boolean} [normalizeAudio] - unused (normalize=0 is always applied); kept for API compatibility
|
| 161 |
*/
|
| 162 |
-
async makeBubble(videoPath, bubble, outputPath, onLog, detachAudio = true, normalizeAudio) {
|
| 163 |
// --- 1. Prepare: strip audio from source so all visual rendering is on a silent video ---
|
| 164 |
const meta0 = await FFMpegUtils.getMediaMetadata(videoPath);
|
| 165 |
const originalHasAudio = !!(meta0 && meta0.audio);
|
|
@@ -172,7 +172,7 @@ class BubbleMaker {
|
|
| 172 |
|
| 173 |
// --- 2. Render all visual overlays onto the silent video (no audio filters needed) ---
|
| 174 |
const visualOutput = tempPath('visual', 'mp4');
|
| 175 |
-
await this._renderVisuals(silentVideo, bubbleArr, visualOutput, onLog);
|
| 176 |
|
| 177 |
// --- 3. Build mixed audio (original + SFX) in one shot ---
|
| 178 |
let finalAudioPath = null;
|
|
@@ -221,17 +221,17 @@ class BubbleMaker {
|
|
| 221 |
* Handles both array (batch or sequential) and single-bubble cases.
|
| 222 |
* No audio filters are emitted here at all.
|
| 223 |
*/
|
| 224 |
-
async _renderVisuals(silentVideo, bubbleArr, outputPath, onLog) {
|
| 225 |
// Expand templates for each bubble
|
| 226 |
const meta = await FFMpegUtils.getMediaMetadata(silentVideo);
|
| 227 |
const vw = meta.video.width;
|
| 228 |
const vh = meta.video.height;
|
| 229 |
bubbleArr.forEach(b => Object.assign(b, this._applyTemplate(b, vw, vh)));
|
| 230 |
const expandedBubbles = bubbleArr;
|
| 231 |
-
const hasHtml = expandedBubbles.some(b => b.bubbleHtml && b.bubbleHtml.text);
|
| 232 |
|
| 233 |
if (hasHtml) {
|
| 234 |
-
await this._renderHtmlBatch(silentVideo, expandedBubbles, outputPath, onLog, meta);
|
| 235 |
}
|
| 236 |
|
| 237 |
const hasText = expandedBubbles.some(b => b.bubbleText && b.bubbleText.text);
|
|
@@ -258,7 +258,7 @@ class BubbleMaker {
|
|
| 258 |
// All media bubbles — try to batch into one ffmpeg pass
|
| 259 |
if (hasMedia) {
|
| 260 |
try {
|
| 261 |
-
await this._renderMediaBatch(silentVideo, expandedBubbles, outputPath, onLog, meta);
|
| 262 |
return;
|
| 263 |
} catch (err) {
|
| 264 |
onLog && onLog('Batch media render failed, falling back to sequential: ' + err);
|
|
@@ -316,8 +316,8 @@ class BubbleMaker {
|
|
| 316 |
return b;
|
| 317 |
}
|
| 318 |
|
| 319 |
-
async _renderHtmlBatch(silentVideo, bubbles, outputPath, onLog, meta) {
|
| 320 |
-
const htmlBubbles = bubbles.filter(b => b.
|
| 321 |
if (htmlBubbles.length === 0) return;
|
| 322 |
|
| 323 |
const htmlRenderer = new HtmlRender();
|
|
@@ -330,13 +330,14 @@ class BubbleMaker {
|
|
| 330 |
|
| 331 |
for (const b of htmlBubbles) {
|
| 332 |
const isVideo = typeof b.durationSec === 'number' && b.durationSec > 0;
|
| 333 |
-
const outPath =
|
| 334 |
|
| 335 |
let finalHtml = null;
|
| 336 |
let finalFile = null;
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
|
|
|
| 340 |
} else {
|
| 341 |
const html = b.bubbleHtml.text;
|
| 342 |
// Wrap HTML to ensure transparent background and full viewport coverage
|
|
@@ -364,7 +365,7 @@ class BubbleMaker {
|
|
| 364 |
|
| 365 |
if (isVideo) {
|
| 366 |
onLog && onLog(`Rendering HTML bubble to video ${outPath} for ${b.durationSec}s`);
|
| 367 |
-
await htmlRenderer.renderVideo(finalHtml, finalFile, outPath, b.durationSec, { viewport: { width: vw, height: vh } });
|
| 368 |
} else {
|
| 369 |
onLog && onLog(`Rendering HTML bubble to image ${outPath}`);
|
| 370 |
await htmlRenderer.renderFrame(finalHtml, finalFile, outPath, { omitBackground: true });
|
|
@@ -389,7 +390,7 @@ class BubbleMaker {
|
|
| 389 |
/**
|
| 390 |
* Render multiple media (non-text) bubbles in a single ffmpeg pass (no audio).
|
| 391 |
*/
|
| 392 |
-
async _renderMediaBatch(silentVideo, bubbles, outputPath, onLog, meta) {
|
| 393 |
const vw = meta.video.width;
|
| 394 |
const vh = meta.video.height;
|
| 395 |
const mainDuration = meta.duration || meta.video?.duration || 0;
|
|
|
|
| 159 |
* @param {boolean} detachAudio - kept for API compatibility; audio is always handled separately now
|
| 160 |
* @param {boolean} [normalizeAudio] - unused (normalize=0 is always applied); kept for API compatibility
|
| 161 |
*/
|
| 162 |
+
async makeBubble(videoPath, bubble, outputPath, onLog, detachAudio = true, normalizeAudio, manuscriptMeta) {
|
| 163 |
// --- 1. Prepare: strip audio from source so all visual rendering is on a silent video ---
|
| 164 |
const meta0 = await FFMpegUtils.getMediaMetadata(videoPath);
|
| 165 |
const originalHasAudio = !!(meta0 && meta0.audio);
|
|
|
|
| 172 |
|
| 173 |
// --- 2. Render all visual overlays onto the silent video (no audio filters needed) ---
|
| 174 |
const visualOutput = tempPath('visual', 'mp4');
|
| 175 |
+
await this._renderVisuals(silentVideo, bubbleArr, visualOutput, onLog, manuscriptMeta);
|
| 176 |
|
| 177 |
// --- 3. Build mixed audio (original + SFX) in one shot ---
|
| 178 |
let finalAudioPath = null;
|
|
|
|
| 221 |
* Handles both array (batch or sequential) and single-bubble cases.
|
| 222 |
* No audio filters are emitted here at all.
|
| 223 |
*/
|
| 224 |
+
async _renderVisuals(silentVideo, bubbleArr, outputPath, onLog, manuscriptMeta) {
|
| 225 |
// Expand templates for each bubble
|
| 226 |
const meta = await FFMpegUtils.getMediaMetadata(silentVideo);
|
| 227 |
const vw = meta.video.width;
|
| 228 |
const vh = meta.video.height;
|
| 229 |
bubbleArr.forEach(b => Object.assign(b, this._applyTemplate(b, vw, vh)));
|
| 230 |
const expandedBubbles = bubbleArr;
|
| 231 |
+
const hasHtml = expandedBubbles.some(b => b.bubbleHtml && (b.bubbleHtml.text || b.bubbleHtml.path));
|
| 232 |
|
| 233 |
if (hasHtml) {
|
| 234 |
+
await this._renderHtmlBatch(silentVideo, expandedBubbles, outputPath, onLog, meta, manuscriptMeta);
|
| 235 |
}
|
| 236 |
|
| 237 |
const hasText = expandedBubbles.some(b => b.bubbleText && b.bubbleText.text);
|
|
|
|
| 258 |
// All media bubbles — try to batch into one ffmpeg pass
|
| 259 |
if (hasMedia) {
|
| 260 |
try {
|
| 261 |
+
await this._renderMediaBatch(silentVideo, expandedBubbles, outputPath, onLog, meta, manuscriptMeta);
|
| 262 |
return;
|
| 263 |
} catch (err) {
|
| 264 |
onLog && onLog('Batch media render failed, falling back to sequential: ' + err);
|
|
|
|
| 316 |
return b;
|
| 317 |
}
|
| 318 |
|
| 319 |
+
async _renderHtmlBatch(silentVideo, bubbles, outputPath, onLog, meta, manuscriptMeta) {
|
| 320 |
+
const htmlBubbles = bubbles.filter(b => (b.bubbleHtml && (b.bubbleHtml.path || b.bubbleHtml.text)));
|
| 321 |
if (htmlBubbles.length === 0) return;
|
| 322 |
|
| 323 |
const htmlRenderer = new HtmlRender();
|
|
|
|
| 330 |
|
| 331 |
for (const b of htmlBubbles) {
|
| 332 |
const isVideo = typeof b.durationSec === 'number' && b.durationSec > 0;
|
| 333 |
+
const outPath = path.join('public', `bubble_${htmlBubbles.indexOf(b)}.` + (isVideo ? 'mp4' : 'png'));
|
| 334 |
|
| 335 |
let finalHtml = null;
|
| 336 |
let finalFile = null;
|
| 337 |
|
| 338 |
+
const htmlPath = b.htmlFile || (b.bubbleHtml && b.bubbleHtml.path);
|
| 339 |
+
if (htmlPath) {
|
| 340 |
+
finalFile = path.isAbsolute(htmlPath) ? htmlPath : path.join(process.cwd(), htmlPath);
|
| 341 |
} else {
|
| 342 |
const html = b.bubbleHtml.text;
|
| 343 |
// Wrap HTML to ensure transparent background and full viewport coverage
|
|
|
|
| 365 |
|
| 366 |
if (isVideo) {
|
| 367 |
onLog && onLog(`Rendering HTML bubble to video ${outPath} for ${b.durationSec}s`);
|
| 368 |
+
await htmlRenderer.renderVideo(finalHtml, finalFile, outPath, b.durationSec, { fps: manuscriptMeta.fps, viewport: { width: vw, height: vh } });
|
| 369 |
} else {
|
| 370 |
onLog && onLog(`Rendering HTML bubble to image ${outPath}`);
|
| 371 |
await htmlRenderer.renderFrame(finalHtml, finalFile, outPath, { omitBackground: true });
|
|
|
|
| 390 |
/**
|
| 391 |
* Render multiple media (non-text) bubbles in a single ffmpeg pass (no audio).
|
| 392 |
*/
|
| 393 |
+
async _renderMediaBatch(silentVideo, bubbles, outputPath, onLog, meta, manuscriptMeta) {
|
| 394 |
const vw = meta.video.width;
|
| 395 |
const vh = meta.video.height;
|
| 396 |
const mainDuration = meta.duration || meta.video?.duration || 0;
|