shiveshnavin commited on
Commit
f357c45
·
1 Parent(s): 08ff55b

Add playwrigt vide render

Browse files
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
- recordVideo: {
38
- dir: path.dirname(outFilePath),
39
- size: options.viewport || { width: 1080, height: 1920 }
 
 
 
 
 
 
 
 
 
 
 
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
- await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle' });
 
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.htmlFile || (b.bubbleHtml && b.bubbleHtml.text));
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 = tempPath('html_bubble', isVideo ? 'mp4' : 'png');
334
 
335
  let finalHtml = null;
336
  let finalFile = null;
337
 
338
- if (b.htmlFile) {
339
- finalFile = path.isAbsolute(b.htmlFile) ? b.htmlFile : path.join(process.cwd(), b.htmlFile);
 
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;