Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
β’
3165afb
1
Parent(s):
94c46c0
let's try dev mode
Browse filesThis view is limited to 50 files because it contains too many changes. Β
See raw diff
- Dockerfile +11 -9
- package-lock.json +33 -139
- package.json +10 -18
- src/bug-in-bun/FIXME.md +10 -0
- src/{core/ffmpeg/getMediaInfo.mts β bug-in-bun/aitube_ffmpeg/analyze/getMediaInfo.ts} +6 -6
- src/bug-in-bun/aitube_ffmpeg/analyze/index.ts +1 -0
- src/{core/ffmpeg/concatenateAudio.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateAudio.ts} +23 -21
- src/{core/ffmpeg/concatenateVideos.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideos.ts} +13 -14
- src/{core/ffmpeg/concatenateVideosAndMergeAudio.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosAndMergeAudio.ts} +9 -12
- src/{core/ffmpeg/concatenateVideosWithAudio.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosWithAudio.ts} +17 -18
- src/{core/ffmpeg/createVideoFromFrames.mts β bug-in-bun/aitube_ffmpeg/concatenate/createVideoFromFrames.ts} +3 -3
- src/bug-in-bun/aitube_ffmpeg/concatenate/index.ts +8 -0
- src/{core/ffmpeg/convertAudioToWav.mts β bug-in-bun/aitube_ffmpeg/convert/convertAudioToWav.ts} +11 -10
- src/{core/ffmpeg/convertMp4ToMp3.mts β bug-in-bun/aitube_ffmpeg/convert/convertMp4ToMp3.ts} +7 -6
- src/{core/ffmpeg/convertMp4ToWebm.mts β bug-in-bun/aitube_ffmpeg/convert/convertMp4ToWebm.ts} +5 -6
- src/bug-in-bun/aitube_ffmpeg/convert/index.ts +3 -0
- src/bug-in-bun/aitube_ffmpeg/index.ts +38 -0
- src/{core/ffmpeg/addImageToVideo.mts β bug-in-bun/aitube_ffmpeg/overlay/addImageToVideo.ts} +7 -6
- src/{core/ffmpeg/addTextToVideo.mts β bug-in-bun/aitube_ffmpeg/overlay/addTextToVideo.ts} +4 -3
- src/{core/ffmpeg/createTextOverlayImage.mts β bug-in-bun/aitube_ffmpeg/overlay/createTextOverlayImage.ts} +2 -2
- src/{core/utils/getCssStyle.mts β bug-in-bun/aitube_ffmpeg/overlay/getCssStyle.ts} +0 -0
- src/{core/converters/htmlToBase64Png.mts β bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64Png.ts} +3 -1
- src/{core/ffmpeg/imageToVideoBase64.mts β bug-in-bun/aitube_ffmpeg/overlay/imageToVideoBase64.ts} +19 -11
- src/bug-in-bun/aitube_ffmpeg/overlay/index.ts +6 -0
- src/{core/ffmpeg/cropBase64Video.mts β bug-in-bun/aitube_ffmpeg/transform/cropBase64Video.ts} +6 -6
- src/{core/ffmpeg/cropVideo.mts β bug-in-bun/aitube_ffmpeg/transform/cropVideo.ts} +8 -8
- src/bug-in-bun/aitube_ffmpeg/transform/index.ts +3 -0
- src/{core/ffmpeg/scaleVideo.mts β bug-in-bun/aitube_ffmpeg/transform/scaleVideo.ts} +7 -10
- src/core/base64/addBase64.mts +0 -51
- src/core/base64/dataUriToBlob.mts +0 -15
- src/core/base64/extractBase64.mts +0 -39
- src/core/converters/blobToWebp.mts +0 -5
- src/core/converters/bufferToJpeg.mts +0 -5
- src/core/converters/bufferToMp3.mts +0 -5
- src/core/converters/bufferToMp4.mts +0 -5
- src/core/converters/bufferToPng.mts +0 -5
- src/core/converters/bufferToWav.mts +0 -5
- src/core/converters/bufferToWebp.mts +0 -5
- src/core/converters/convertImageTo.mts +0 -31
- src/core/converters/convertImageToJpeg.mts +0 -27
- src/core/converters/convertImageToOriginal.mts +0 -6
- src/core/converters/convertImageToPng.mts +0 -23
- src/core/converters/convertImageToWebp.mts +0 -41
- src/core/converters/imageFormats.mts +0 -1
- src/core/exporters/{clapWithStoryboardsToVideoFile.mts β clapWithStoryboardsToVideoFile.ts} +2 -2
- src/core/exporters/{clapWithVideosToVideoFile.mts β clapWithVideosToVideoFile.ts} +2 -3
- src/core/exporters/{storyboardSegmentToVideoFile.mts β storyboardSegmentToVideoFile.ts} +5 -7
- src/core/exporters/{videoSegmentToVideoFile.mts β videoSegmentToVideoFile.ts} +5 -7
- src/core/files/deleteFile.mts +0 -14
- src/core/files/deleteFileWithName.mts +0 -12
Dockerfile
CHANGED
@@ -5,6 +5,9 @@ ARG DEBIAN_FRONTEND=noninteractive
|
|
5 |
|
6 |
RUN apk update
|
7 |
|
|
|
|
|
|
|
8 |
RUN apk add alpine-sdk pkgconfig
|
9 |
|
10 |
# For FFMPEG and gl concat
|
@@ -16,32 +19,31 @@ RUN apk add build-base gcompat udev ttf-opensans chromium
|
|
16 |
RUN apk add ffmpeg
|
17 |
|
18 |
# Set up a new user named "user" with user ID 1000
|
19 |
-
RUN adduser --disabled-password --uid
|
20 |
|
21 |
# Switch to the "user" user
|
22 |
USER user
|
23 |
|
24 |
# Set home to the user's home directory
|
25 |
-
ENV
|
26 |
-
PATH=/home/user/.local/bin:$PATH
|
27 |
|
28 |
# Set the working directory to the user's home directory
|
29 |
-
WORKDIR
|
30 |
|
31 |
# Install app dependencies
|
32 |
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
33 |
# where available (npm@5+)
|
34 |
-
|
35 |
|
36 |
# make sure the .env is copied as well
|
37 |
-
COPY --chown=user .env
|
38 |
|
39 |
RUN ffmpeg -version
|
40 |
|
41 |
-
|
|
|
42 |
|
43 |
-
|
44 |
-
COPY --chown=user . $HOME/app
|
45 |
|
46 |
EXPOSE 7860
|
47 |
|
|
|
5 |
|
6 |
RUN apk update
|
7 |
|
8 |
+
# for dev mode
|
9 |
+
RUN apk add git git-lfs get procps htop vim nano
|
10 |
+
|
11 |
RUN apk add alpine-sdk pkgconfig
|
12 |
|
13 |
# For FFMPEG and gl concat
|
|
|
19 |
RUN apk add ffmpeg
|
20 |
|
21 |
# Set up a new user named "user" with user ID 1000
|
22 |
+
RUN adduser --disabled-password --uid 1000 user
|
23 |
|
24 |
# Switch to the "user" user
|
25 |
USER user
|
26 |
|
27 |
# Set home to the user's home directory
|
28 |
+
ENV PATH=.local/bin:$PATH
|
|
|
29 |
|
30 |
# Set the working directory to the user's home directory
|
31 |
+
WORKDIR /app
|
32 |
|
33 |
# Install app dependencies
|
34 |
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
35 |
# where available (npm@5+)
|
36 |
+
COPYY --link --chown=user package*.json /app
|
37 |
|
38 |
# make sure the .env is copied as well
|
39 |
+
COPY --link --chown=user .env /app
|
40 |
|
41 |
RUN ffmpeg -version
|
42 |
|
43 |
+
# Copy the current directory contents into the container at /app setting the owner to the user
|
44 |
+
COPY --link --chown=user . $HOME/app
|
45 |
|
46 |
+
RUN npm ci
|
|
|
47 |
|
48 |
EXPOSE 7860
|
49 |
|
package-lock.json
CHANGED
@@ -10,27 +10,20 @@
|
|
10 |
"license": "Apache License",
|
11 |
"dependencies": {
|
12 |
"@aitube/clap": "0.0.7",
|
|
|
|
|
13 |
"@types/express": "^4.17.17",
|
14 |
"@types/fluent-ffmpeg": "^2.1.24",
|
15 |
"@types/uuid": "^9.0.2",
|
16 |
"dotenv": "^16.3.1",
|
17 |
-
"eventsource-parser": "^1.0.0",
|
18 |
"express": "^4.18.2",
|
19 |
"fluent-ffmpeg": "^2.1.2",
|
20 |
-
"
|
21 |
-
"mime-types": "^2.1.35",
|
22 |
-
"node-fetch": "^3.3.1",
|
23 |
-
"puppeteer": "^22.7.0",
|
24 |
"query-string": "^9.0.0",
|
25 |
"sharp": "^0.33.3",
|
26 |
-
"
|
27 |
-
"ts-node": "^10.9.1",
|
28 |
-
"type-fest": "^4.8.2",
|
29 |
-
"uuid": "^9.0.0",
|
30 |
-
"yaml": "^2.4.1"
|
31 |
},
|
32 |
"devDependencies": {
|
33 |
-
"@types/mime-types": "^2.1.4",
|
34 |
"@types/node": "^20.12.7",
|
35 |
"tsx": "^4.7.0"
|
36 |
}
|
@@ -47,6 +40,24 @@
|
|
47 |
"typescript": "^5.4.5"
|
48 |
}
|
49 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
"node_modules/@babel/code-frame": {
|
51 |
"version": "7.24.2",
|
52 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
|
@@ -60,19 +71,19 @@
|
|
60 |
}
|
61 |
},
|
62 |
"node_modules/@babel/helper-validator-identifier": {
|
63 |
-
"version": "7.
|
64 |
-
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.
|
65 |
-
"integrity": "sha512-
|
66 |
"engines": {
|
67 |
"node": ">=6.9.0"
|
68 |
}
|
69 |
},
|
70 |
"node_modules/@babel/highlight": {
|
71 |
-
"version": "7.24.
|
72 |
-
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.
|
73 |
-
"integrity": "sha512-
|
74 |
"dependencies": {
|
75 |
-
"@babel/helper-validator-identifier": "^7.
|
76 |
"chalk": "^2.4.2",
|
77 |
"js-tokens": "^4.0.0",
|
78 |
"picocolors": "^1.0.0"
|
@@ -1046,12 +1057,6 @@
|
|
1046 |
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
1047 |
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
1048 |
},
|
1049 |
-
"node_modules/@types/mime-types": {
|
1050 |
-
"version": "2.1.4",
|
1051 |
-
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
1052 |
-
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
|
1053 |
-
"dev": true
|
1054 |
-
},
|
1055 |
"node_modules/@types/node": {
|
1056 |
"version": "20.12.7",
|
1057 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
@@ -1529,11 +1534,11 @@
|
|
1529 |
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
1530 |
},
|
1531 |
"node_modules/data-uri-to-buffer": {
|
1532 |
-
"version": "
|
1533 |
-
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-
|
1534 |
-
"integrity": "sha512-
|
1535 |
"engines": {
|
1536 |
-
"node": ">=
|
1537 |
}
|
1538 |
},
|
1539 |
"node_modules/debug": {
|
@@ -1806,14 +1811,6 @@
|
|
1806 |
"node": ">= 0.6"
|
1807 |
}
|
1808 |
},
|
1809 |
-
"node_modules/eventsource-parser": {
|
1810 |
-
"version": "1.1.2",
|
1811 |
-
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
|
1812 |
-
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
|
1813 |
-
"engines": {
|
1814 |
-
"node": ">=14.18"
|
1815 |
-
}
|
1816 |
-
},
|
1817 |
"node_modules/express": {
|
1818 |
"version": "4.19.2",
|
1819 |
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
@@ -1908,28 +1905,6 @@
|
|
1908 |
"pend": "~1.2.0"
|
1909 |
}
|
1910 |
},
|
1911 |
-
"node_modules/fetch-blob": {
|
1912 |
-
"version": "3.2.0",
|
1913 |
-
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
1914 |
-
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
1915 |
-
"funding": [
|
1916 |
-
{
|
1917 |
-
"type": "github",
|
1918 |
-
"url": "https://github.com/sponsors/jimmywarting"
|
1919 |
-
},
|
1920 |
-
{
|
1921 |
-
"type": "paypal",
|
1922 |
-
"url": "https://paypal.me/jimmywarting"
|
1923 |
-
}
|
1924 |
-
],
|
1925 |
-
"dependencies": {
|
1926 |
-
"node-domexception": "^1.0.0",
|
1927 |
-
"web-streams-polyfill": "^3.0.3"
|
1928 |
-
},
|
1929 |
-
"engines": {
|
1930 |
-
"node": "^12.20 || >= 14.13"
|
1931 |
-
}
|
1932 |
-
},
|
1933 |
"node_modules/filter-obj": {
|
1934 |
"version": "5.1.0",
|
1935 |
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
|
@@ -1970,17 +1945,6 @@
|
|
1970 |
"node": ">=0.8.0"
|
1971 |
}
|
1972 |
},
|
1973 |
-
"node_modules/formdata-polyfill": {
|
1974 |
-
"version": "4.0.10",
|
1975 |
-
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
1976 |
-
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
1977 |
-
"dependencies": {
|
1978 |
-
"fetch-blob": "^3.1.2"
|
1979 |
-
},
|
1980 |
-
"engines": {
|
1981 |
-
"node": ">=12.20.0"
|
1982 |
-
}
|
1983 |
-
},
|
1984 |
"node_modules/forwarded": {
|
1985 |
"version": "0.2.0",
|
1986 |
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
@@ -2098,14 +2062,6 @@
|
|
2098 |
"node": ">= 14"
|
2099 |
}
|
2100 |
},
|
2101 |
-
"node_modules/get-uri/node_modules/data-uri-to-buffer": {
|
2102 |
-
"version": "6.0.2",
|
2103 |
-
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
2104 |
-
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
|
2105 |
-
"engines": {
|
2106 |
-
"node": ">= 14"
|
2107 |
-
}
|
2108 |
-
},
|
2109 |
"node_modules/get-uri/node_modules/debug": {
|
2110 |
"version": "4.3.4",
|
2111 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
@@ -2496,41 +2452,6 @@
|
|
2496 |
"node": ">= 0.4.0"
|
2497 |
}
|
2498 |
},
|
2499 |
-
"node_modules/node-domexception": {
|
2500 |
-
"version": "1.0.0",
|
2501 |
-
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
2502 |
-
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
2503 |
-
"funding": [
|
2504 |
-
{
|
2505 |
-
"type": "github",
|
2506 |
-
"url": "https://github.com/sponsors/jimmywarting"
|
2507 |
-
},
|
2508 |
-
{
|
2509 |
-
"type": "github",
|
2510 |
-
"url": "https://paypal.me/jimmywarting"
|
2511 |
-
}
|
2512 |
-
],
|
2513 |
-
"engines": {
|
2514 |
-
"node": ">=10.5.0"
|
2515 |
-
}
|
2516 |
-
},
|
2517 |
-
"node_modules/node-fetch": {
|
2518 |
-
"version": "3.3.2",
|
2519 |
-
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
2520 |
-
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
2521 |
-
"dependencies": {
|
2522 |
-
"data-uri-to-buffer": "^4.0.0",
|
2523 |
-
"fetch-blob": "^3.1.4",
|
2524 |
-
"formdata-polyfill": "^4.0.10"
|
2525 |
-
},
|
2526 |
-
"engines": {
|
2527 |
-
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
2528 |
-
},
|
2529 |
-
"funding": {
|
2530 |
-
"type": "opencollective",
|
2531 |
-
"url": "https://opencollective.com/node-fetch"
|
2532 |
-
}
|
2533 |
-
},
|
2534 |
"node_modules/object-inspect": {
|
2535 |
"version": "1.13.1",
|
2536 |
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
@@ -3217,14 +3138,6 @@
|
|
3217 |
"streamx": "^2.15.0"
|
3218 |
}
|
3219 |
},
|
3220 |
-
"node_modules/temp-dir": {
|
3221 |
-
"version": "3.0.0",
|
3222 |
-
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz",
|
3223 |
-
"integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==",
|
3224 |
-
"engines": {
|
3225 |
-
"node": ">=14.16"
|
3226 |
-
}
|
3227 |
-
},
|
3228 |
"node_modules/through": {
|
3229 |
"version": "2.3.8",
|
3230 |
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
@@ -3304,17 +3217,6 @@
|
|
3304 |
"fsevents": "~2.3.3"
|
3305 |
}
|
3306 |
},
|
3307 |
-
"node_modules/type-fest": {
|
3308 |
-
"version": "4.18.0",
|
3309 |
-
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.0.tgz",
|
3310 |
-
"integrity": "sha512-+dbmiyliDY/2TTcjCS7NpI9yV2iEFlUDk5TKnsbkN7ZoRu5s7bT+zvYtNFhFXC2oLwURGT2frACAZvbbyNBI+w==",
|
3311 |
-
"engines": {
|
3312 |
-
"node": ">=16"
|
3313 |
-
},
|
3314 |
-
"funding": {
|
3315 |
-
"url": "https://github.com/sponsors/sindresorhus"
|
3316 |
-
}
|
3317 |
-
},
|
3318 |
"node_modules/type-is": {
|
3319 |
"version": "1.6.18",
|
3320 |
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
@@ -3408,14 +3310,6 @@
|
|
3408 |
"node": ">= 0.8"
|
3409 |
}
|
3410 |
},
|
3411 |
-
"node_modules/web-streams-polyfill": {
|
3412 |
-
"version": "3.3.3",
|
3413 |
-
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
3414 |
-
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
3415 |
-
"engines": {
|
3416 |
-
"node": ">= 8"
|
3417 |
-
}
|
3418 |
-
},
|
3419 |
"node_modules/which": {
|
3420 |
"version": "1.3.1",
|
3421 |
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
|
|
10 |
"license": "Apache License",
|
11 |
"dependencies": {
|
12 |
"@aitube/clap": "0.0.7",
|
13 |
+
"@aitube/encoders": "0.0.0",
|
14 |
+
"@aitube/io": "0.0.0",
|
15 |
"@types/express": "^4.17.17",
|
16 |
"@types/fluent-ffmpeg": "^2.1.24",
|
17 |
"@types/uuid": "^9.0.2",
|
18 |
"dotenv": "^16.3.1",
|
|
|
19 |
"express": "^4.18.2",
|
20 |
"fluent-ffmpeg": "^2.1.2",
|
21 |
+
"puppeteer": "^22.7.1",
|
|
|
|
|
|
|
22 |
"query-string": "^9.0.0",
|
23 |
"sharp": "^0.33.3",
|
24 |
+
"ts-node": "^10.9.1"
|
|
|
|
|
|
|
|
|
25 |
},
|
26 |
"devDependencies": {
|
|
|
27 |
"@types/node": "^20.12.7",
|
28 |
"tsx": "^4.7.0"
|
29 |
}
|
|
|
40 |
"typescript": "^5.4.5"
|
41 |
}
|
42 |
},
|
43 |
+
"node_modules/@aitube/encoders": {
|
44 |
+
"version": "0.0.0",
|
45 |
+
"resolved": "https://registry.npmjs.org/@aitube/encoders/-/encoders-0.0.0.tgz",
|
46 |
+
"integrity": "sha512-jKIii0m0Lwr6l+slA9cPg/c8WCl2uylNYON74VU+3cgi//7Yt34Ym1afGIjQd2vEWzPR7LrlHcrb4F0War7Fmg==",
|
47 |
+
"peerDependencies": {
|
48 |
+
"typescript": "^5.4.5"
|
49 |
+
}
|
50 |
+
},
|
51 |
+
"node_modules/@aitube/io": {
|
52 |
+
"version": "0.0.0",
|
53 |
+
"resolved": "https://registry.npmjs.org/@aitube/io/-/io-0.0.0.tgz",
|
54 |
+
"integrity": "sha512-ay/h4VZGmlS3ZHraHHLkeXovXEv6WU2SSZp+n27rgasnoDsAU8If2IXgZXxIjnpqOigZ8qed/GhmCzvhqvmOrQ==",
|
55 |
+
"dependencies": {
|
56 |
+
"mime-types": "^2.1.35",
|
57 |
+
"sharp": "^0.33.3",
|
58 |
+
"uuid": "^9.0.1"
|
59 |
+
}
|
60 |
+
},
|
61 |
"node_modules/@babel/code-frame": {
|
62 |
"version": "7.24.2",
|
63 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
|
|
|
71 |
}
|
72 |
},
|
73 |
"node_modules/@babel/helper-validator-identifier": {
|
74 |
+
"version": "7.24.5",
|
75 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
|
76 |
+
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
|
77 |
"engines": {
|
78 |
"node": ">=6.9.0"
|
79 |
}
|
80 |
},
|
81 |
"node_modules/@babel/highlight": {
|
82 |
+
"version": "7.24.5",
|
83 |
+
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz",
|
84 |
+
"integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==",
|
85 |
"dependencies": {
|
86 |
+
"@babel/helper-validator-identifier": "^7.24.5",
|
87 |
"chalk": "^2.4.2",
|
88 |
"js-tokens": "^4.0.0",
|
89 |
"picocolors": "^1.0.0"
|
|
|
1057 |
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
1058 |
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
1059 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
1060 |
"node_modules/@types/node": {
|
1061 |
"version": "20.12.7",
|
1062 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
|
|
1534 |
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
1535 |
},
|
1536 |
"node_modules/data-uri-to-buffer": {
|
1537 |
+
"version": "6.0.2",
|
1538 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
1539 |
+
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
|
1540 |
"engines": {
|
1541 |
+
"node": ">= 14"
|
1542 |
}
|
1543 |
},
|
1544 |
"node_modules/debug": {
|
|
|
1811 |
"node": ">= 0.6"
|
1812 |
}
|
1813 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1814 |
"node_modules/express": {
|
1815 |
"version": "4.19.2",
|
1816 |
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
|
|
1905 |
"pend": "~1.2.0"
|
1906 |
}
|
1907 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1908 |
"node_modules/filter-obj": {
|
1909 |
"version": "5.1.0",
|
1910 |
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
|
|
|
1945 |
"node": ">=0.8.0"
|
1946 |
}
|
1947 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1948 |
"node_modules/forwarded": {
|
1949 |
"version": "0.2.0",
|
1950 |
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
|
|
2062 |
"node": ">= 14"
|
2063 |
}
|
2064 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2065 |
"node_modules/get-uri/node_modules/debug": {
|
2066 |
"version": "4.3.4",
|
2067 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
|
|
2452 |
"node": ">= 0.4.0"
|
2453 |
}
|
2454 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2455 |
"node_modules/object-inspect": {
|
2456 |
"version": "1.13.1",
|
2457 |
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
|
|
3138 |
"streamx": "^2.15.0"
|
3139 |
}
|
3140 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3141 |
"node_modules/through": {
|
3142 |
"version": "2.3.8",
|
3143 |
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
|
|
3217 |
"fsevents": "~2.3.3"
|
3218 |
}
|
3219 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3220 |
"node_modules/type-is": {
|
3221 |
"version": "1.6.18",
|
3222 |
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
|
|
3310 |
"node": ">= 0.8"
|
3311 |
}
|
3312 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3313 |
"node_modules/which": {
|
3314 |
"version": "1.3.1",
|
3315 |
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
package.json
CHANGED
@@ -1,41 +1,33 @@
|
|
1 |
{
|
2 |
"name": "ai-tube-clap-exporter",
|
3 |
"version": "1.0.0",
|
4 |
-
"description": "
|
5 |
-
"main": "src/index.
|
6 |
"scripts": {
|
7 |
-
"start": "tsx src/index.
|
8 |
-
"dev": "tsx src/index.
|
9 |
"docker": "npm run docker:build && npm run docker:run",
|
10 |
-
"docker:build": "docker build -t ai-tube-
|
11 |
-
"docker:run": "docker run -it -p 7860:7860 ai-tube-
|
12 |
-
"alchemy:test": "tsx src/core/alchemy/test.mts"
|
13 |
},
|
14 |
"author": "Julian Bilcke <[email protected]>",
|
15 |
"license": "Apache License",
|
16 |
"dependencies": {
|
17 |
"@aitube/clap": "0.0.7",
|
|
|
|
|
18 |
"@types/express": "^4.17.17",
|
19 |
"@types/fluent-ffmpeg": "^2.1.24",
|
20 |
"@types/uuid": "^9.0.2",
|
21 |
"dotenv": "^16.3.1",
|
22 |
-
"eventsource-parser": "^1.0.0",
|
23 |
"express": "^4.18.2",
|
24 |
"fluent-ffmpeg": "^2.1.2",
|
25 |
-
"
|
26 |
-
"mime-types": "^2.1.35",
|
27 |
-
"node-fetch": "^3.3.1",
|
28 |
-
"puppeteer": "^22.7.0",
|
29 |
"query-string": "^9.0.0",
|
30 |
"sharp": "^0.33.3",
|
31 |
-
"
|
32 |
-
"ts-node": "^10.9.1",
|
33 |
-
"type-fest": "^4.8.2",
|
34 |
-
"uuid": "^9.0.0",
|
35 |
-
"yaml": "^2.4.1"
|
36 |
},
|
37 |
"devDependencies": {
|
38 |
-
"@types/mime-types": "^2.1.4",
|
39 |
"@types/node": "^20.12.7",
|
40 |
"tsx": "^4.7.0"
|
41 |
}
|
|
|
1 |
{
|
2 |
"name": "ai-tube-clap-exporter",
|
3 |
"version": "1.0.0",
|
4 |
+
"description": "API service to convert a .clap (will all its assets) to a video file",
|
5 |
+
"main": "src/index.ts",
|
6 |
"scripts": {
|
7 |
+
"start": "tsx src/index.ts",
|
8 |
+
"dev": "tsx src/index.ts",
|
9 |
"docker": "npm run docker:build && npm run docker:run",
|
10 |
+
"docker:build": "docker build -t ai-tube-clap-exporter .",
|
11 |
+
"docker:run": "docker run -it -p 7860:7860 ai-tube-clap-exporter"
|
|
|
12 |
},
|
13 |
"author": "Julian Bilcke <[email protected]>",
|
14 |
"license": "Apache License",
|
15 |
"dependencies": {
|
16 |
"@aitube/clap": "0.0.7",
|
17 |
+
"@aitube/encoders": "0.0.0",
|
18 |
+
"@aitube/io": "0.0.0",
|
19 |
"@types/express": "^4.17.17",
|
20 |
"@types/fluent-ffmpeg": "^2.1.24",
|
21 |
"@types/uuid": "^9.0.2",
|
22 |
"dotenv": "^16.3.1",
|
|
|
23 |
"express": "^4.18.2",
|
24 |
"fluent-ffmpeg": "^2.1.2",
|
25 |
+
"puppeteer": "^22.7.1",
|
|
|
|
|
|
|
26 |
"query-string": "^9.0.0",
|
27 |
"sharp": "^0.33.3",
|
28 |
+
"ts-node": "^10.9.1"
|
|
|
|
|
|
|
|
|
29 |
},
|
30 |
"devDependencies": {
|
|
|
31 |
"@types/node": "^20.12.7",
|
32 |
"tsx": "^4.7.0"
|
33 |
}
|
src/bug-in-bun/FIXME.md
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
We should use import .... from "@aitube/ffmpeg"
|
2 |
+
|
3 |
+
But there is a bug with Bun:
|
4 |
+
|
5 |
+
https://github.com/oven-sh/bun/issues/4477
|
6 |
+
|
7 |
+
|
8 |
+
Once fixed, we can delete aitube_ffmpeg (and the dependencies like fluent-ffmpeg, puppeteer etc)
|
9 |
+
|
10 |
+
and only use @aitube/ffmpeg
|
src/{core/ffmpeg/getMediaInfo.mts β bug-in-bun/aitube_ffmpeg/analyze/getMediaInfo.ts}
RENAMED
@@ -1,8 +1,8 @@
|
|
1 |
-
import
|
|
|
|
|
2 |
|
3 |
-
import
|
4 |
-
import { promises as fs } from "node:fs";
|
5 |
-
import { join } from "node:path";
|
6 |
|
7 |
export type MediaMetadata = {
|
8 |
durationInSec: number;
|
@@ -31,13 +31,13 @@ export async function getMediaInfo(input: string): Promise<MediaMetadata> {
|
|
31 |
const tempFileName = join(tmpdir(), `temp-media-${Date.now()}`);
|
32 |
|
33 |
// Write the buffer to a temporary file
|
34 |
-
await
|
35 |
|
36 |
// Get metadata from the temporary file then delete the file
|
37 |
try {
|
38 |
return await getMetaDataFromPath(tempFileName);
|
39 |
} finally {
|
40 |
-
await
|
41 |
}
|
42 |
}
|
43 |
|
|
|
1 |
+
import { tmpdir } from "node:os"
|
2 |
+
import { writeFile, rm } from "node:fs/promises"
|
3 |
+
import { join } from "node:path"
|
4 |
|
5 |
+
import ffmpeg from "fluent-ffmpeg"
|
|
|
|
|
6 |
|
7 |
export type MediaMetadata = {
|
8 |
durationInSec: number;
|
|
|
31 |
const tempFileName = join(tmpdir(), `temp-media-${Date.now()}`);
|
32 |
|
33 |
// Write the buffer to a temporary file
|
34 |
+
await writeFile(tempFileName, buffer);
|
35 |
|
36 |
// Get metadata from the temporary file then delete the file
|
37 |
try {
|
38 |
return await getMetaDataFromPath(tempFileName);
|
39 |
} finally {
|
40 |
+
await rm(tempFileName);
|
41 |
}
|
42 |
}
|
43 |
|
src/bug-in-bun/aitube_ffmpeg/analyze/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { getMediaInfo } from "./getMediaInfo"
|
src/{core/ffmpeg/concatenateAudio.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateAudio.ts}
RENAMED
@@ -1,13 +1,12 @@
|
|
1 |
-
import { existsSync
|
2 |
-
import os from "node:os"
|
3 |
import path from "node:path"
|
4 |
|
5 |
-
import { v4 as uuidv4 } from "uuid"
|
6 |
-
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
7 |
-
import { writeBase64ToFile } from "
|
8 |
-
import {
|
9 |
-
|
10 |
-
import {
|
11 |
|
12 |
export type ConcatenateAudioOptions = {
|
13 |
// those are base64 audio strings!
|
@@ -34,20 +33,19 @@ export async function concatenateAudio({
|
|
34 |
throw new Error("Audios must be provided in an array");
|
35 |
}
|
36 |
|
37 |
-
const tempDir =
|
38 |
-
await fs.mkdir(tempDir);
|
39 |
|
40 |
// console.log(" |- created tmp dir")
|
41 |
|
42 |
// trivial case: there is only one audio to concatenate!
|
43 |
if (audioTracks.length === 1 && audioTracks[0]) {
|
44 |
const audioTrack = audioTracks[0]
|
45 |
-
const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`)
|
46 |
-
await writeBase64ToFile(addBase64Header(audioTrack, "wav"), outputFilePath)
|
47 |
|
48 |
// console.log(" |- there is only one track! so.. returning that")
|
49 |
-
const { durationInSec } = await getMediaInfo(outputFilePath)
|
50 |
-
return { filepath: outputFilePath, durationInSec }
|
51 |
}
|
52 |
|
53 |
if (audioFilePaths.length === 1) {
|
@@ -60,10 +58,12 @@ export async function concatenateAudio({
|
|
60 |
for (const track of audioTracks) {
|
61 |
if (!track) { continue }
|
62 |
const audioFilePath = path.join(tempDir, `audio_${++i}.wav`);
|
63 |
-
await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath)
|
|
|
64 |
audioFilePaths.push(audioFilePath);
|
65 |
}
|
66 |
|
|
|
67 |
audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio))
|
68 |
|
69 |
const outputFilePath = output ?? path.join(tempDir, `${uuidv4()}.${outputFormat}`);
|
@@ -77,7 +77,7 @@ export async function concatenateAudio({
|
|
77 |
prevLabel = nextLabel;
|
78 |
}
|
79 |
|
80 |
-
|
81 |
console.log(" |- concatenateAudio(): DEBUG:", {
|
82 |
tempDir,
|
83 |
audioFilePaths,
|
@@ -85,13 +85,13 @@ export async function concatenateAudio({
|
|
85 |
filterComplex,
|
86 |
prevLabel
|
87 |
})
|
|
|
88 |
|
89 |
let cmd: FfmpegCommand = ffmpeg() // .outputOptions('-vn');
|
90 |
|
91 |
audioFilePaths.forEach((audio, i) => {
|
92 |
-
cmd = cmd.input(audio)
|
93 |
-
})
|
94 |
-
|
95 |
|
96 |
const promise = new Promise<ConcatenateAudioOutput>((resolve, reject) => {
|
97 |
cmd = cmd
|
@@ -99,10 +99,12 @@ export async function concatenateAudio({
|
|
99 |
.on('end', async () => {
|
100 |
try {
|
101 |
const { durationInSec } = await getMediaInfo(outputFilePath);
|
|
|
102 |
// console.log("concatenation ended! see ->", outputFilePath)
|
103 |
-
resolve({ filepath: outputFilePath, durationInSec })
|
|
|
104 |
} catch (err) {
|
105 |
-
reject(err)
|
106 |
}
|
107 |
})
|
108 |
.complexFilter(filterComplex, prevLabel)
|
|
|
1 |
+
import { existsSync } from "node:fs"
|
|
|
2 |
import path from "node:path"
|
3 |
|
4 |
+
import { v4 as uuidv4 } from "uuid"
|
5 |
+
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
6 |
+
import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io"
|
7 |
+
import { addBase64Header } from "@aitube/encoders"
|
8 |
+
|
9 |
+
import { getMediaInfo } from "../analyze/getMediaInfo"
|
10 |
|
11 |
export type ConcatenateAudioOptions = {
|
12 |
// those are base64 audio strings!
|
|
|
33 |
throw new Error("Audios must be provided in an array");
|
34 |
}
|
35 |
|
36 |
+
const tempDir = await getRandomDirectory()
|
|
|
37 |
|
38 |
// console.log(" |- created tmp dir")
|
39 |
|
40 |
// trivial case: there is only one audio to concatenate!
|
41 |
if (audioTracks.length === 1 && audioTracks[0]) {
|
42 |
const audioTrack = audioTracks[0]
|
43 |
+
const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`)
|
44 |
+
await writeBase64ToFile(addBase64Header(audioTrack, "wav"), outputFilePath)
|
45 |
|
46 |
// console.log(" |- there is only one track! so.. returning that")
|
47 |
+
const { durationInSec } = await getMediaInfo(outputFilePath)
|
48 |
+
return { filepath: outputFilePath, durationInSec }
|
49 |
}
|
50 |
|
51 |
if (audioFilePaths.length === 1) {
|
|
|
58 |
for (const track of audioTracks) {
|
59 |
if (!track) { continue }
|
60 |
const audioFilePath = path.join(tempDir, `audio_${++i}.wav`);
|
61 |
+
await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath)
|
62 |
+
|
63 |
audioFilePaths.push(audioFilePath);
|
64 |
}
|
65 |
|
66 |
+
// TODO: convert this to an async filter using promises
|
67 |
audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio))
|
68 |
|
69 |
const outputFilePath = output ?? path.join(tempDir, `${uuidv4()}.${outputFormat}`);
|
|
|
77 |
prevLabel = nextLabel;
|
78 |
}
|
79 |
|
80 |
+
/*
|
81 |
console.log(" |- concatenateAudio(): DEBUG:", {
|
82 |
tempDir,
|
83 |
audioFilePaths,
|
|
|
85 |
filterComplex,
|
86 |
prevLabel
|
87 |
})
|
88 |
+
*/
|
89 |
|
90 |
let cmd: FfmpegCommand = ffmpeg() // .outputOptions('-vn');
|
91 |
|
92 |
audioFilePaths.forEach((audio, i) => {
|
93 |
+
cmd = cmd.input(audio)
|
94 |
+
})
|
|
|
95 |
|
96 |
const promise = new Promise<ConcatenateAudioOutput>((resolve, reject) => {
|
97 |
cmd = cmd
|
|
|
99 |
.on('end', async () => {
|
100 |
try {
|
101 |
const { durationInSec } = await getMediaInfo(outputFilePath);
|
102 |
+
|
103 |
// console.log("concatenation ended! see ->", outputFilePath)
|
104 |
+
resolve({ filepath: outputFilePath, durationInSec })
|
105 |
+
|
106 |
} catch (err) {
|
107 |
+
reject(err)
|
108 |
}
|
109 |
})
|
110 |
.complexFilter(filterComplex, prevLabel)
|
src/{core/ffmpeg/concatenateVideos.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideos.ts}
RENAMED
@@ -1,11 +1,11 @@
|
|
1 |
-
import { existsSync
|
2 |
-
import
|
3 |
-
import path from "node:path";
|
4 |
|
5 |
-
import { v4 as uuidv4 } from "uuid"
|
6 |
-
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
|
|
7 |
|
8 |
-
import { getMediaInfo } from "
|
9 |
|
10 |
export type ConcatenateVideoOutput = {
|
11 |
filepath: string;
|
@@ -22,22 +22,21 @@ export async function concatenateVideos({
|
|
22 |
videoFilePaths: string[];
|
23 |
}): Promise<ConcatenateVideoOutput> {
|
24 |
if (!Array.isArray(videoFilePaths)) {
|
25 |
-
throw new Error("Videos must be provided in an array")
|
26 |
}
|
27 |
|
28 |
videoFilePaths = videoFilePaths.filter((videoPath) => existsSync(videoPath))
|
29 |
|
30 |
// Create a temporary working directory
|
31 |
-
const tempDir =
|
32 |
-
await fs.mkdir(tempDir);
|
33 |
|
34 |
-
const filePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`)
|
35 |
|
36 |
if (!filePath) {
|
37 |
-
throw new Error("Failed to generate a valid temporary file path")
|
38 |
}
|
39 |
|
40 |
-
let cmd: FfmpegCommand = ffmpeg()
|
41 |
|
42 |
videoFilePaths.forEach((video) => {
|
43 |
cmd = cmd.addInput(video)
|
@@ -57,5 +56,5 @@ export async function concatenateVideos({
|
|
57 |
})
|
58 |
.mergeToFile(filePath, tempDir);
|
59 |
}
|
60 |
-
)
|
61 |
-
}
|
|
|
1 |
+
import { existsSync } from "node:fs"
|
2 |
+
import path from "node:path"
|
|
|
3 |
|
4 |
+
import { v4 as uuidv4 } from "uuid"
|
5 |
+
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
6 |
+
import { getRandomDirectory } from "@aitube/io"
|
7 |
|
8 |
+
import { getMediaInfo } from "../analyze/getMediaInfo"
|
9 |
|
10 |
export type ConcatenateVideoOutput = {
|
11 |
filepath: string;
|
|
|
22 |
videoFilePaths: string[];
|
23 |
}): Promise<ConcatenateVideoOutput> {
|
24 |
if (!Array.isArray(videoFilePaths)) {
|
25 |
+
throw new Error("Videos must be provided in an array")
|
26 |
}
|
27 |
|
28 |
videoFilePaths = videoFilePaths.filter((videoPath) => existsSync(videoPath))
|
29 |
|
30 |
// Create a temporary working directory
|
31 |
+
const tempDir = await getRandomDirectory()
|
|
|
32 |
|
33 |
+
const filePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`)
|
34 |
|
35 |
if (!filePath) {
|
36 |
+
throw new Error("Failed to generate a valid temporary file path")
|
37 |
}
|
38 |
|
39 |
+
let cmd: FfmpegCommand = ffmpeg()
|
40 |
|
41 |
videoFilePaths.forEach((video) => {
|
42 |
cmd = cmd.addInput(video)
|
|
|
56 |
})
|
57 |
.mergeToFile(filePath, tempDir);
|
58 |
}
|
59 |
+
)
|
60 |
+
}
|
src/{core/ffmpeg/concatenateVideosAndMergeAudio.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosAndMergeAudio.ts}
RENAMED
@@ -1,15 +1,13 @@
|
|
1 |
-
import { existsSync
|
2 |
-
import os from "node:os"
|
3 |
import path from "node:path"
|
4 |
|
5 |
-
import { v4 as uuidv4 } from "uuid"
|
6 |
-
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
7 |
-
import {
|
8 |
-
import {
|
9 |
-
|
10 |
-
import {
|
11 |
-
import {
|
12 |
-
import { extractBase64 } from "../base64/extractBase64.mts";
|
13 |
|
14 |
type ConcatenateVideoAndMergeAudioOptions = {
|
15 |
output?: string;
|
@@ -36,8 +34,7 @@ export const concatenateVideosAndMergeAudio = async ({
|
|
36 |
|
37 |
try {
|
38 |
// Prepare temporary directories
|
39 |
-
const tempDir =
|
40 |
-
await fs.mkdir(tempDir);
|
41 |
|
42 |
let i = 0
|
43 |
for (const audioTrack of audioTracks) {
|
|
|
1 |
+
import { existsSync } from "node:fs"
|
|
|
2 |
import path from "node:path"
|
3 |
|
4 |
+
import { v4 as uuidv4 } from "uuid"
|
5 |
+
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
6 |
+
import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io"
|
7 |
+
import { addBase64Header, extractBase64 } from "@aitube/encoders"
|
8 |
+
|
9 |
+
import { getMediaInfo } from "../analyze/getMediaInfo"
|
10 |
+
import { concatenateVideos } from "./concatenateVideos"
|
|
|
11 |
|
12 |
type ConcatenateVideoAndMergeAudioOptions = {
|
13 |
output?: string;
|
|
|
34 |
|
35 |
try {
|
36 |
// Prepare temporary directories
|
37 |
+
const tempDir = await getRandomDirectory()
|
|
|
38 |
|
39 |
let i = 0
|
40 |
for (const audioTrack of audioTracks) {
|
src/{core/ffmpeg/concatenateVideosWithAudio.mts β bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosWithAudio.ts}
RENAMED
@@ -1,15 +1,14 @@
|
|
1 |
-
import { existsSync
|
2 |
-
import
|
3 |
import path from "node:path"
|
4 |
|
5 |
-
import { v4 as uuidv4 } from "uuid"
|
6 |
-
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
7 |
-
import {
|
8 |
-
import { writeBase64ToFile } from "
|
9 |
-
|
10 |
-
import {
|
11 |
-
import {
|
12 |
-
import { extractBase64 } from "../base64/extractBase64.mts";
|
13 |
|
14 |
export type SupportedExportFormat = "mp4" | "webm"
|
15 |
export const defaultExportFormat = "mp4"
|
@@ -41,14 +40,14 @@ export const concatenateVideosWithAudio = async ({
|
|
41 |
|
42 |
try {
|
43 |
// Prepare temporary directories
|
44 |
-
const tempDir =
|
45 |
-
await fs.mkdir(tempDir);
|
46 |
|
47 |
if (audioTrack && audioTrack.length > 0) {
|
48 |
const analysis = extractBase64(audioTrack)
|
49 |
// console.log(`concatenateVideosWithAudio: writing down an audio file (${analysis.extension}) from the supplied base64 track`)
|
50 |
-
audioFilePath = path.join(tempDir, `audio.${analysis.extension}`)
|
51 |
-
|
|
|
52 |
}
|
53 |
|
54 |
// Decode and concatenate base64 video tracks to temporary file
|
@@ -58,13 +57,13 @@ export const concatenateVideosWithAudio = async ({
|
|
58 |
// note: here we assume the input video is in mp4
|
59 |
|
60 |
const analysis = extractBase64(audioTrack)
|
61 |
-
const videoFilePath = path.join(tempDir, `video${++i}.${analysis.extension}`)
|
62 |
|
63 |
// console.log(`concatenateVideosWithAudio: writing down a video file (${analysis.extension}) from the supplied base64 track`)
|
64 |
|
65 |
-
await writeBase64ToFile(addBase64Header(track, analysis.extension), videoFilePath)
|
66 |
|
67 |
-
videoFilePaths.push(videoFilePath)
|
68 |
}
|
69 |
|
70 |
videoFilePaths = videoFilePaths.filter((video) => existsSync(video))
|
@@ -162,7 +161,7 @@ export const concatenateVideosWithAudio = async ({
|
|
162 |
try {
|
163 |
if (asBase64) {
|
164 |
try {
|
165 |
-
const outputBuffer = await
|
166 |
const outputBase64 = addBase64Header(outputBuffer.toString("base64"), format)
|
167 |
resolve(outputBase64);
|
168 |
} catch (error) {
|
|
|
1 |
+
import { existsSync } from "node:fs"
|
2 |
+
import { readFile } from "node:fs/promises"
|
3 |
import path from "node:path"
|
4 |
|
5 |
+
import { v4 as uuidv4 } from "uuid"
|
6 |
+
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
|
7 |
+
import { addBase64Header, extractBase64 } from "@aitube/encoders"
|
8 |
+
import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io"
|
9 |
+
|
10 |
+
import { getMediaInfo } from "../analyze/getMediaInfo"
|
11 |
+
import { concatenateVideos } from "./concatenateVideos"
|
|
|
12 |
|
13 |
export type SupportedExportFormat = "mp4" | "webm"
|
14 |
export const defaultExportFormat = "mp4"
|
|
|
40 |
|
41 |
try {
|
42 |
// Prepare temporary directories
|
43 |
+
const tempDir = await getRandomDirectory()
|
|
|
44 |
|
45 |
if (audioTrack && audioTrack.length > 0) {
|
46 |
const analysis = extractBase64(audioTrack)
|
47 |
// console.log(`concatenateVideosWithAudio: writing down an audio file (${analysis.extension}) from the supplied base64 track`)
|
48 |
+
audioFilePath = path.join(tempDir, `audio.${analysis.extension}`)
|
49 |
+
|
50 |
+
await writeBase64ToFile(addBase64Header(audioTrack, analysis.extension), audioFilePath)
|
51 |
}
|
52 |
|
53 |
// Decode and concatenate base64 video tracks to temporary file
|
|
|
57 |
// note: here we assume the input video is in mp4
|
58 |
|
59 |
const analysis = extractBase64(audioTrack)
|
60 |
+
const videoFilePath = path.join(tempDir, `video${++i}.${analysis.extension}`)
|
61 |
|
62 |
// console.log(`concatenateVideosWithAudio: writing down a video file (${analysis.extension}) from the supplied base64 track`)
|
63 |
|
64 |
+
await writeBase64ToFile(addBase64Header(track, analysis.extension), videoFilePath)
|
65 |
|
66 |
+
videoFilePaths.push(videoFilePath)
|
67 |
}
|
68 |
|
69 |
videoFilePaths = videoFilePaths.filter((video) => existsSync(video))
|
|
|
161 |
try {
|
162 |
if (asBase64) {
|
163 |
try {
|
164 |
+
const outputBuffer = await readFile(finalOutputFilePath);
|
165 |
const outputBase64 = addBase64Header(outputBuffer.toString("base64"), format)
|
166 |
resolve(outputBase64);
|
167 |
} catch (error) {
|
src/{core/ffmpeg/createVideoFromFrames.mts β bug-in-bun/aitube_ffmpeg/concatenate/createVideoFromFrames.ts}
RENAMED
@@ -5,7 +5,7 @@ import path from "node:path"
|
|
5 |
import ffmpeg from "fluent-ffmpeg"
|
6 |
import { v4 as uuidv4 } from "uuid"
|
7 |
|
8 |
-
import { getMediaInfo } from "
|
9 |
|
10 |
export async function createVideoFromFrames({
|
11 |
inputFramesDirectory,
|
@@ -19,7 +19,7 @@ export async function createVideoFromFrames({
|
|
19 |
// 3. grain has too much entropy and cannot be compressed, so it multiplies by 5 the size weight
|
20 |
grainAmount = 0, // Optional parameter for film grain (eg. 10)
|
21 |
|
22 |
-
inputVideoToUseAsAudio, // Optional parameter for audio input (need to be a mp4, but it can be a base64 data URI or a file path)
|
23 |
|
24 |
debug = false,
|
25 |
|
@@ -42,7 +42,7 @@ export async function createVideoFromFrames({
|
|
42 |
|
43 |
|
44 |
// Construct the input frame pattern
|
45 |
-
const inputFramePattern = path.join(inputFramesDirectory, framesFilePattern);
|
46 |
|
47 |
|
48 |
// Create a temporary working directory
|
|
|
5 |
import ffmpeg from "fluent-ffmpeg"
|
6 |
import { v4 as uuidv4 } from "uuid"
|
7 |
|
8 |
+
import { getMediaInfo } from "../analyze/getMediaInfo"
|
9 |
|
10 |
export async function createVideoFromFrames({
|
11 |
inputFramesDirectory,
|
|
|
19 |
// 3. grain has too much entropy and cannot be compressed, so it multiplies by 5 the size weight
|
20 |
grainAmount = 0, // Optional parameter for film grain (eg. 10)
|
21 |
|
22 |
+
inputVideoToUseAsAudio = "", // Optional parameter for audio input (need to be a mp4, but it can be a base64 data URI or a file path)
|
23 |
|
24 |
debug = false,
|
25 |
|
|
|
42 |
|
43 |
|
44 |
// Construct the input frame pattern
|
45 |
+
const inputFramePattern = path.join(inputFramesDirectory, framesFilePattern || "");
|
46 |
|
47 |
|
48 |
// Create a temporary working directory
|
src/bug-in-bun/aitube_ffmpeg/concatenate/index.ts
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export { concatenateAudio } from "./concatenateAudio"
|
2 |
+
export type { ConcatenateAudioOutput } from "./concatenateAudio"
|
3 |
+
export { concatenateVideos } from "./concatenateVideos"
|
4 |
+
export { concatenateVideosAndMergeAudio } from "./concatenateVideosAndMergeAudio"
|
5 |
+
export { concatenateVideosWithAudio } from "./concatenateVideosWithAudio"
|
6 |
+
export { defaultExportFormat } from "./concatenateVideosWithAudio"
|
7 |
+
export type { SupportedExportFormat } from "./concatenateVideosWithAudio"
|
8 |
+
export { createVideoFromFrames } from "./createVideoFromFrames"
|
src/{core/ffmpeg/convertAudioToWav.mts β bug-in-bun/aitube_ffmpeg/convert/convertAudioToWav.ts}
RENAMED
@@ -1,8 +1,9 @@
|
|
1 |
-
import {
|
2 |
-
import os from "node:os"
|
3 |
-
import path from "node:path"
|
4 |
-
import
|
5 |
-
|
|
|
6 |
|
7 |
type ConvertAudioToWavParams = {
|
8 |
input: string;
|
@@ -27,21 +28,21 @@ export async function convertAudioToWav({
|
|
27 |
|
28 |
const inputBuffer = Buffer.from(matches[2], "base64");
|
29 |
const inputFormat = matches[1]; // Either 'mp3' or 'wav'
|
30 |
-
const tempDir = await
|
31 |
inputAudioPath = path.join(tempDir, `temp.${inputFormat}`);
|
32 |
|
33 |
// Write the base64 data to the temporary file
|
34 |
-
await
|
35 |
} else {
|
36 |
// Verify that the input file exists
|
37 |
-
if (!(await
|
38 |
throw new Error(`Input audio file does not exist: ${inputAudioPath}`);
|
39 |
}
|
40 |
}
|
41 |
|
42 |
// If no output path is provided, create a temporary file for the output
|
43 |
if (!outputAudioPath) {
|
44 |
-
const tempDir = await
|
45 |
outputAudioPath = path.join(tempDir, `${path.parse(inputAudioPath).name}.wav`);
|
46 |
}
|
47 |
|
@@ -54,7 +55,7 @@ export async function convertAudioToWav({
|
|
54 |
.on("end", async () => {
|
55 |
if (asBase64) {
|
56 |
try {
|
57 |
-
const audioBuffer = await
|
58 |
const audioBase64 = `data:audio/wav;base64,${audioBuffer.toString("base64")}`;
|
59 |
resolve(audioBase64);
|
60 |
} catch (error) {
|
|
|
1 |
+
import { mkdtemp, writeFile, readFile, stat } from "node:fs/promises"
|
2 |
+
import os from "node:os"
|
3 |
+
import path from "node:path"
|
4 |
+
import { Buffer } from "node:buffer"
|
5 |
+
|
6 |
+
import ffmpeg from "fluent-ffmpeg"
|
7 |
|
8 |
type ConvertAudioToWavParams = {
|
9 |
input: string;
|
|
|
28 |
|
29 |
const inputBuffer = Buffer.from(matches[2], "base64");
|
30 |
const inputFormat = matches[1]; // Either 'mp3' or 'wav'
|
31 |
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-input-"));
|
32 |
inputAudioPath = path.join(tempDir, `temp.${inputFormat}`);
|
33 |
|
34 |
// Write the base64 data to the temporary file
|
35 |
+
await writeFile(inputAudioPath, inputBuffer);
|
36 |
} else {
|
37 |
// Verify that the input file exists
|
38 |
+
if (!(await stat(inputAudioPath)).isFile()) {
|
39 |
throw new Error(`Input audio file does not exist: ${inputAudioPath}`);
|
40 |
}
|
41 |
}
|
42 |
|
43 |
// If no output path is provided, create a temporary file for the output
|
44 |
if (!outputAudioPath) {
|
45 |
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-output-"));
|
46 |
outputAudioPath = path.join(tempDir, `${path.parse(inputAudioPath).name}.wav`);
|
47 |
}
|
48 |
|
|
|
55 |
.on("end", async () => {
|
56 |
if (asBase64) {
|
57 |
try {
|
58 |
+
const audioBuffer = await readFile(outputAudioPath);
|
59 |
const audioBase64 = `data:audio/wav;base64,${audioBuffer.toString("base64")}`;
|
60 |
resolve(audioBase64);
|
61 |
} catch (error) {
|
src/{core/ffmpeg/convertMp4ToMp3.mts β bug-in-bun/aitube_ffmpeg/convert/convertMp4ToMp3.ts}
RENAMED
@@ -1,10 +1,11 @@
|
|
1 |
|
2 |
-
import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"
|
3 |
-
import os from "node:os"
|
4 |
-
import path from "node:path"
|
5 |
-
import
|
6 |
-
import {
|
7 |
-
|
|
|
8 |
|
9 |
export async function convertMp4ToMp3({
|
10 |
input,
|
|
|
1 |
|
2 |
+
import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"
|
3 |
+
import os from "node:os"
|
4 |
+
import path from "node:path"
|
5 |
+
import { tmpdir } from "node:os"
|
6 |
+
import { Buffer } from "node:buffer"
|
7 |
+
|
8 |
+
import ffmpeg from "fluent-ffmpeg"
|
9 |
|
10 |
export async function convertMp4ToMp3({
|
11 |
input,
|
src/{core/ffmpeg/convertMp4ToWebm.mts β bug-in-bun/aitube_ffmpeg/convert/convertMp4ToWebm.ts}
RENAMED
@@ -1,11 +1,10 @@
|
|
1 |
|
2 |
-
import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"
|
3 |
-
import path from "node:path"
|
|
|
|
|
4 |
|
5 |
-
import
|
6 |
-
import { Buffer } from "node:buffer";
|
7 |
-
|
8 |
-
import ffmpeg from "fluent-ffmpeg";
|
9 |
|
10 |
export async function convertMp4ToWebm({
|
11 |
input,
|
|
|
1 |
|
2 |
+
import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"
|
3 |
+
import path from "node:path"
|
4 |
+
import { tmpdir } from "node:os"
|
5 |
+
import { Buffer } from "node:buffer"
|
6 |
|
7 |
+
import ffmpeg from "fluent-ffmpeg"
|
|
|
|
|
|
|
8 |
|
9 |
export async function convertMp4ToWebm({
|
10 |
input,
|
src/bug-in-bun/aitube_ffmpeg/convert/index.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export { convertAudioToWav } from "./convertAudioToWav"
|
2 |
+
export { convertMp4ToMp3 } from "./convertMp4ToMp3"
|
3 |
+
export { convertMp4ToWebm } from "./convertMp4ToWebm"
|
src/bug-in-bun/aitube_ffmpeg/index.ts
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export {
|
2 |
+
getMediaInfo
|
3 |
+
} from "./analyze"
|
4 |
+
|
5 |
+
export {
|
6 |
+
concatenateAudio,
|
7 |
+
concatenateVideos,
|
8 |
+
concatenateVideosAndMergeAudio,
|
9 |
+
concatenateVideosWithAudio,
|
10 |
+
defaultExportFormat,
|
11 |
+
createVideoFromFrames
|
12 |
+
} from "./concatenate"
|
13 |
+
|
14 |
+
export type {
|
15 |
+
SupportedExportFormat,
|
16 |
+
ConcatenateAudioOutput
|
17 |
+
} from "./concatenate"
|
18 |
+
|
19 |
+
export {
|
20 |
+
convertAudioToWav,
|
21 |
+
convertMp4ToMp3,
|
22 |
+
convertMp4ToWebm,
|
23 |
+
} from "./convert"
|
24 |
+
|
25 |
+
export {
|
26 |
+
addImageToVideo,
|
27 |
+
addTextToVideo,
|
28 |
+
createTextOverlayImage,
|
29 |
+
getCssStyle,
|
30 |
+
htmlToBase64Png,
|
31 |
+
imageToVideoBase64,
|
32 |
+
} from "./overlay"
|
33 |
+
|
34 |
+
export {
|
35 |
+
cropBase64Video,
|
36 |
+
cropVideo,
|
37 |
+
scaleVideo,
|
38 |
+
} from "./transform"
|
src/{core/ffmpeg/addImageToVideo.mts β bug-in-bun/aitube_ffmpeg/overlay/addImageToVideo.ts}
RENAMED
@@ -1,8 +1,9 @@
|
|
1 |
-
import {
|
2 |
-
import
|
3 |
-
|
4 |
-
import ffmpeg from "fluent-ffmpeg"
|
5 |
-
import { v4 as uuidv4 } from "uuid"
|
|
|
6 |
|
7 |
type AddImageToVideoParams = {
|
8 |
inputVideoPath: string;
|
@@ -25,7 +26,7 @@ export async function addImageToVideo({
|
|
25 |
|
26 |
// If no output path is provided, create a temporary file for output
|
27 |
if (!outputVideoPath) {
|
28 |
-
const tempDir = await
|
29 |
outputVideoPath = path.join(tempDir, `${uuidv4()}.mp4`);
|
30 |
}
|
31 |
|
|
|
1 |
+
import { existsSync } from "node:fs"
|
2 |
+
import path from "node:path"
|
3 |
+
|
4 |
+
import ffmpeg from "fluent-ffmpeg"
|
5 |
+
import { v4 as uuidv4 } from "uuid"
|
6 |
+
import { getRandomDirectory } from "@aitube/io"
|
7 |
|
8 |
type AddImageToVideoParams = {
|
9 |
inputVideoPath: string;
|
|
|
26 |
|
27 |
// If no output path is provided, create a temporary file for output
|
28 |
if (!outputVideoPath) {
|
29 |
+
const tempDir = await getRandomDirectory()
|
30 |
outputVideoPath = path.join(tempDir, `${uuidv4()}.mp4`);
|
31 |
}
|
32 |
|
src/{core/ffmpeg/addTextToVideo.mts β bug-in-bun/aitube_ffmpeg/overlay/addTextToVideo.ts}
RENAMED
@@ -1,6 +1,7 @@
|
|
1 |
-
import {
|
2 |
-
|
3 |
-
import {
|
|
|
4 |
|
5 |
export async function addTextToVideo({
|
6 |
inputVideoPath,
|
|
|
1 |
+
import { deleteFile } from "@aitube/io"
|
2 |
+
|
3 |
+
import { createTextOverlayImage } from "./createTextOverlayImage"
|
4 |
+
import { addImageToVideo } from "./addImageToVideo"
|
5 |
|
6 |
export async function addTextToVideo({
|
7 |
inputVideoPath,
|
src/{core/ffmpeg/createTextOverlayImage.mts β bug-in-bun/aitube_ffmpeg/overlay/createTextOverlayImage.ts}
RENAMED
@@ -1,6 +1,6 @@
|
|
1 |
|
2 |
-
import { TextOverlayFont, TextOverlayFontWeight, TextOverlayPosition, TextOverlayStyle, getCssStyle } from "
|
3 |
-
import { htmlToBase64Png } from "
|
4 |
|
5 |
// generate a PNG overlay using HTML
|
6 |
// most sizes are in percentage of the image height
|
|
|
1 |
|
2 |
+
import { TextOverlayFont, TextOverlayFontWeight, TextOverlayPosition, TextOverlayStyle, getCssStyle } from "./getCssStyle"
|
3 |
+
import { htmlToBase64Png } from "./htmlToBase64Png"
|
4 |
|
5 |
// generate a PNG overlay using HTML
|
6 |
// most sizes are in percentage of the image height
|
src/{core/utils/getCssStyle.mts β bug-in-bun/aitube_ffmpeg/overlay/getCssStyle.ts}
RENAMED
File without changes
|
src/{core/converters/htmlToBase64Png.mts β bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64Png.ts}
RENAMED
@@ -7,7 +7,7 @@ import puppeteer from "puppeteer"
|
|
7 |
|
8 |
export async function htmlToBase64Png({
|
9 |
outputImagePath,
|
10 |
-
html,
|
11 |
width = 800,
|
12 |
height = 600,
|
13 |
}: {
|
@@ -53,6 +53,8 @@ export async function htmlToBase64Png({
|
|
53 |
|
54 |
const content = await page.$("body")
|
55 |
|
|
|
|
|
56 |
const buffer = await content.screenshot({
|
57 |
path: outputImagePath,
|
58 |
omitBackground: true,
|
|
|
7 |
|
8 |
export async function htmlToBase64Png({
|
9 |
outputImagePath,
|
10 |
+
html = "",
|
11 |
width = 800,
|
12 |
height = 600,
|
13 |
}: {
|
|
|
53 |
|
54 |
const content = await page.$("body")
|
55 |
|
56 |
+
if (!content) { throw new Error (`coudln't find body content`) }
|
57 |
+
|
58 |
const buffer = await content.screenshot({
|
59 |
path: outputImagePath,
|
60 |
omitBackground: true,
|
src/{core/ffmpeg/imageToVideoBase64.mts β bug-in-bun/aitube_ffmpeg/overlay/imageToVideoBase64.ts}
RENAMED
@@ -1,8 +1,8 @@
|
|
1 |
-
import { rm,
|
2 |
-
import
|
3 |
-
|
4 |
-
import ffmpeg from "fluent-ffmpeg"
|
5 |
-
import { getRandomDirectory } from "
|
6 |
|
7 |
/**
|
8 |
* Converts an image in Base64 format to a video encoded in Base64.
|
@@ -82,13 +82,21 @@ export async function imageToVideoBase64({
|
|
82 |
'-pix_fmt yuv420p'
|
83 |
])
|
84 |
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
91 |
return ffmpegCommand
|
|
|
|
|
|
|
92 |
.on('end', () => resolve())
|
93 |
.on('error', (err) => reject(err))
|
94 |
.save(outputFilePath);
|
|
|
1 |
+
import { rm, writeFile, readFile } from "node:fs/promises"
|
2 |
+
import path from "node:path"
|
3 |
+
|
4 |
+
import ffmpeg from "fluent-ffmpeg"
|
5 |
+
import { getRandomDirectory } from "@aitube/io"
|
6 |
|
7 |
/**
|
8 |
* Converts an image in Base64 format to a video encoded in Base64.
|
|
|
82 |
'-pix_fmt yuv420p'
|
83 |
])
|
84 |
|
85 |
+
if (zoomInRatePerSecond > 0) {
|
86 |
+
const zoomIncreasePerSecond = zoomInRatePerSecond / 100;
|
87 |
+
const totalZoomFactor = 1 + (zoomIncreasePerSecond * durationInSeconds);
|
88 |
+
const framesTotal = durationInSeconds * fps;
|
89 |
+
const zoomPerFrame = zoomIncreasePerSecond / fps;
|
90 |
+
|
91 |
+
const zoomFormula = `if(lte(zoom\\,${totalZoomFactor}),zoom+${zoomPerFrame}\\,zoom)`;
|
92 |
+
|
93 |
+
ffmpegCommand = ffmpegCommand.videoFilters(`zoompan=z='${zoomFormula}':d=${framesTotal}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'`);
|
94 |
+
}
|
95 |
+
|
96 |
return ffmpegCommand
|
97 |
+
.on('start', function(commandLine) {
|
98 |
+
console.log('imageToVideoBase64: Spawned Ffmpeg with command: ' + commandLine);
|
99 |
+
})
|
100 |
.on('end', () => resolve())
|
101 |
.on('error', (err) => reject(err))
|
102 |
.save(outputFilePath);
|
src/bug-in-bun/aitube_ffmpeg/overlay/index.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export { addImageToVideo } from "./addImageToVideo"
|
2 |
+
export { addTextToVideo } from "./addTextToVideo"
|
3 |
+
export { createTextOverlayImage } from "./createTextOverlayImage"
|
4 |
+
export { getCssStyle } from "./getCssStyle"
|
5 |
+
export { htmlToBase64Png } from "./htmlToBase64Png"
|
6 |
+
export { imageToVideoBase64 } from "./imageToVideoBase64"
|
src/{core/ffmpeg/cropBase64Video.mts β bug-in-bun/aitube_ffmpeg/transform/cropBase64Video.ts}
RENAMED
@@ -1,8 +1,8 @@
|
|
1 |
-
import { promises as fs } from "node:fs"
|
2 |
-
import os from "node:os"
|
3 |
-
import path from "node:path"
|
4 |
|
5 |
-
import ffmpeg from "fluent-ffmpeg"
|
6 |
|
7 |
export async function cropBase64Video({
|
8 |
base64Video,
|
@@ -42,8 +42,8 @@ export async function cropBase64Video({
|
|
42 |
}
|
43 |
|
44 |
const { width: inWidth, height: inHeight } = videoStream;
|
45 |
-
const x = Math.floor((inWidth - width) / 2);
|
46 |
-
const y = Math.floor((inHeight - height) / 2);
|
47 |
|
48 |
ffmpeg(inputVideoPath)
|
49 |
.outputOptions([
|
|
|
1 |
+
import { promises as fs } from "node:fs"
|
2 |
+
import os from "node:os"
|
3 |
+
import path from "node:path"
|
4 |
|
5 |
+
import ffmpeg from "fluent-ffmpeg"
|
6 |
|
7 |
export async function cropBase64Video({
|
8 |
base64Video,
|
|
|
42 |
}
|
43 |
|
44 |
const { width: inWidth, height: inHeight } = videoStream;
|
45 |
+
const x = Math.floor(((inWidth || 0) - width) / 2);
|
46 |
+
const y = Math.floor(((inHeight || 0) - height) / 2);
|
47 |
|
48 |
ffmpeg(inputVideoPath)
|
49 |
.outputOptions([
|
src/{core/ffmpeg/cropVideo.mts β bug-in-bun/aitube_ffmpeg/transform/cropVideo.ts}
RENAMED
@@ -1,9 +1,8 @@
|
|
1 |
-
import { promises as fs } from "node:fs"
|
2 |
-
|
3 |
-
import
|
4 |
-
import path from "node:path";
|
5 |
|
6 |
-
import ffmpeg from "fluent-ffmpeg"
|
7 |
|
8 |
export async function cropVideo({
|
9 |
inputVideoPath,
|
@@ -42,9 +41,10 @@ export async function cropVideo({
|
|
42 |
return;
|
43 |
}
|
44 |
|
45 |
-
const { width: inWidth, height: inHeight } = videoStream
|
46 |
-
|
47 |
-
const
|
|
|
48 |
|
49 |
ffmpeg(inputVideoPath)
|
50 |
.outputOptions([
|
|
|
1 |
+
import { promises as fs } from "node:fs"
|
2 |
+
import os from "node:os"
|
3 |
+
import path from "node:path"
|
|
|
4 |
|
5 |
+
import ffmpeg from "fluent-ffmpeg"
|
6 |
|
7 |
export async function cropVideo({
|
8 |
inputVideoPath,
|
|
|
41 |
return;
|
42 |
}
|
43 |
|
44 |
+
const { width: inWidth, height: inHeight } = videoStream
|
45 |
+
|
46 |
+
const x = Math.floor(((inWidth || 0) - width) / 2)
|
47 |
+
const y = Math.floor(((inHeight || 0) - height) / 2)
|
48 |
|
49 |
ffmpeg(inputVideoPath)
|
50 |
.outputOptions([
|
src/bug-in-bun/aitube_ffmpeg/transform/index.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export { cropBase64Video } from "./cropBase64Video"
|
2 |
+
export { cropVideo } from "./cropVideo"
|
3 |
+
export { scaleVideo } from "./scaleVideo"
|
src/{core/ffmpeg/scaleVideo.mts β bug-in-bun/aitube_ffmpeg/transform/scaleVideo.ts}
RENAMED
@@ -1,10 +1,9 @@
|
|
1 |
-
import
|
2 |
-
import
|
3 |
-
import
|
4 |
-
import path from 'node:path';
|
5 |
|
6 |
-
import { v4 as uuidv4 } from "uuid"
|
7 |
-
import ffmpeg from 'fluent-ffmpeg'
|
8 |
|
9 |
export type ScaleVideoParams = {
|
10 |
input: string;
|
@@ -25,8 +24,6 @@ export type ScaleVideoParams = {
|
|
25 |
* 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.
|
26 |
* 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.
|
27 |
*
|
28 |
-
* Enter your message...
|
29 |
-
*
|
30 |
* @param param0
|
31 |
* @returns
|
32 |
*/
|
@@ -36,7 +33,7 @@ export async function scaleVideo({
|
|
36 |
asBase64 = false,
|
37 |
debug = false
|
38 |
}: ScaleVideoParams): Promise<string> {
|
39 |
-
const tempDir = await
|
40 |
const tempOutPath = path.join(tempDir, `${uuidv4()}.mp4`);
|
41 |
|
42 |
let inputPath;
|
@@ -82,7 +79,7 @@ export async function scaleVideo({
|
|
82 |
reject(new Error(`Error loading the video file: ${error}`));
|
83 |
} finally {
|
84 |
// Clean up temporary files
|
85 |
-
await
|
86 |
}
|
87 |
})
|
88 |
.save(tempOutPath);
|
|
|
1 |
+
import { rm, mkdtemp, writeFile, readFile } from 'node:fs/promises'
|
2 |
+
import os from 'node:os'
|
3 |
+
import path from 'node:path'
|
|
|
4 |
|
5 |
+
import { v4 as uuidv4 } from "uuid"
|
6 |
+
import ffmpeg from 'fluent-ffmpeg'
|
7 |
|
8 |
export type ScaleVideoParams = {
|
9 |
input: string;
|
|
|
24 |
* 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.
|
25 |
* 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.
|
26 |
*
|
|
|
|
|
27 |
* @param param0
|
28 |
* @returns
|
29 |
*/
|
|
|
33 |
asBase64 = false,
|
34 |
debug = false
|
35 |
}: ScaleVideoParams): Promise<string> {
|
36 |
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-"));
|
37 |
const tempOutPath = path.join(tempDir, `${uuidv4()}.mp4`);
|
38 |
|
39 |
let inputPath;
|
|
|
79 |
reject(new Error(`Error loading the video file: ${error}`));
|
80 |
} finally {
|
81 |
// Clean up temporary files
|
82 |
+
await rm(tempDir, { recursive: true });
|
83 |
}
|
84 |
})
|
85 |
.save(tempOutPath);
|
src/core/base64/addBase64.mts
DELETED
@@ -1,51 +0,0 @@
|
|
1 |
-
export function addBase64Header(
|
2 |
-
image?: string,
|
3 |
-
format?:
|
4 |
-
| "jpeg" | "jpg" | "png" | "webp" | "heic"
|
5 |
-
| "mp3" | "wav"
|
6 |
-
| "mp4" | "webm"
|
7 |
-
| string
|
8 |
-
) {
|
9 |
-
|
10 |
-
if (!image || typeof image !== "string" || image.length < 60) {
|
11 |
-
return ""
|
12 |
-
}
|
13 |
-
|
14 |
-
const ext = (`${format || ""}`.split(".").pop() || "").toLowerCase().trim()
|
15 |
-
|
16 |
-
let mime = ""
|
17 |
-
if (
|
18 |
-
ext === "jpeg" ||
|
19 |
-
ext === "jpg") {
|
20 |
-
mime = "image/jpeg"
|
21 |
-
} else if (
|
22 |
-
ext === "webp"
|
23 |
-
) {
|
24 |
-
mime = "image/webp"
|
25 |
-
} else if (
|
26 |
-
ext === "png") {
|
27 |
-
mime = "image/png"
|
28 |
-
} else if (ext === "heic") {
|
29 |
-
mime = "image/heic"
|
30 |
-
} else if (ext === "mp3") {
|
31 |
-
mime = "audio/mp3"
|
32 |
-
} else if (ext === "mp4") {
|
33 |
-
mime = "video/mp4"
|
34 |
-
} else if (ext === "webm") {
|
35 |
-
mime = "video/webm"
|
36 |
-
} else if (ext === "wav") {
|
37 |
-
mime = "audio/wav"
|
38 |
-
} else {
|
39 |
-
throw new Error(`addBase64Header failed (unsupported format: ${format})`)
|
40 |
-
}
|
41 |
-
|
42 |
-
if (image.startsWith('data:')) {
|
43 |
-
if (image.startsWith(`data:${mime};base64,`)) {
|
44 |
-
return image
|
45 |
-
} else {
|
46 |
-
throw new Error(`addBase64Header failed (input string is NOT a ${mime} image)`)
|
47 |
-
}
|
48 |
-
} else {
|
49 |
-
return `data:${mime};base64,${image}`
|
50 |
-
}
|
51 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/base64/dataUriToBlob.mts
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
|
2 |
-
export function dataUriToBlob(dataURI = "", defaultContentType = ""): Blob {
|
3 |
-
dataURI = dataURI.replace(/^data:/, '');
|
4 |
-
|
5 |
-
const type = dataURI.match(/(?:image|application|video|audio|text)\/[^;]+/)?.[0] || defaultContentType;
|
6 |
-
const base64 = dataURI.replace(/^[^,]+,/, '');
|
7 |
-
const arrayBuffer = new ArrayBuffer(base64.length);
|
8 |
-
const typedArray = new Uint8Array(arrayBuffer);
|
9 |
-
|
10 |
-
for (let i = 0; i < base64.length; i++) {
|
11 |
-
typedArray[i] = base64.charCodeAt(i);
|
12 |
-
}
|
13 |
-
|
14 |
-
return new Blob([arrayBuffer], { type });
|
15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/base64/extractBase64.mts
DELETED
@@ -1,39 +0,0 @@
|
|
1 |
-
/**
|
2 |
-
* break a base64 string into sub-components
|
3 |
-
*/
|
4 |
-
export function extractBase64(base64: string = ""): {
|
5 |
-
|
6 |
-
// file format eg. video/mp4 text/html audio/wave
|
7 |
-
mimetype: string;
|
8 |
-
|
9 |
-
// file extension eg. .mp4 .html .wav
|
10 |
-
extension: string;
|
11 |
-
|
12 |
-
data: string;
|
13 |
-
buffer: Buffer;
|
14 |
-
blob: Blob;
|
15 |
-
} {
|
16 |
-
// console.log(`extractBase64(${base64.slice(0, 120)})`)
|
17 |
-
// Regular expression to extract the MIME type and the base64 data
|
18 |
-
const matches = base64.match(/^data:([A-Za-z-+0-9/]+);base64,(.+)$/)
|
19 |
-
|
20 |
-
if (!matches || matches.length !== 3) {
|
21 |
-
throw new Error("Invalid base64 string")
|
22 |
-
}
|
23 |
-
|
24 |
-
const mimetype = matches[1] || ""
|
25 |
-
const data = matches[2] || ""
|
26 |
-
const buffer = Buffer.from(data, "base64")
|
27 |
-
const blob = new Blob([buffer])
|
28 |
-
|
29 |
-
// this should be enough for most media formats (jpeg, png, webp, mp4)
|
30 |
-
const extension = mimetype.split("/").pop() || ""
|
31 |
-
|
32 |
-
return {
|
33 |
-
mimetype,
|
34 |
-
extension,
|
35 |
-
data,
|
36 |
-
buffer,
|
37 |
-
blob,
|
38 |
-
}
|
39 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/blobToWebp.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function blobToWebp(blob: Blob) {
|
4 |
-
return addBase64Header(Buffer.from(await blob.text()).toString('base64'), "webp")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/bufferToJpeg.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function bufferToJpeg(buffer: Buffer) {
|
4 |
-
return addBase64Header(buffer.toString('base64'), "jpeg")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/bufferToMp3.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function bufferToMp3(buffer: Buffer) {
|
4 |
-
return addBase64Header(buffer.toString('base64'), "mp3")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/bufferToMp4.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function bufferToMp4(buffer: Buffer) {
|
4 |
-
return addBase64Header(buffer.toString('base64'), "mp4")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/bufferToPng.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function bufferToPng(buffer: Buffer) {
|
4 |
-
return addBase64Header(buffer.toString('base64'), "png")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/bufferToWav.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function bufferToWav(buffer: Buffer) {
|
4 |
-
return addBase64Header(buffer.toString('base64'), "wav")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/bufferToWebp.mts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import { addBase64Header } from "../base64/addBase64.mts";
|
2 |
-
|
3 |
-
export async function bufferToWebp(buffer: Buffer) {
|
4 |
-
return addBase64Header(buffer.toString('base64'), "webp")
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/convertImageTo.mts
DELETED
@@ -1,31 +0,0 @@
|
|
1 |
-
import { convertImageToJpeg } from "./convertImageToJpeg.mts"
|
2 |
-
import { convertImageToPng } from "./convertImageToPng.mts"
|
3 |
-
import { convertImageToWebp } from "./convertImageToWebp.mts"
|
4 |
-
import { ImageFileExt } from "./imageFormats.mts"
|
5 |
-
|
6 |
-
/**
|
7 |
-
* Convert an image to one of the supported file formats
|
8 |
-
*
|
9 |
-
* @param imgBase64
|
10 |
-
* @param outputFormat
|
11 |
-
* @returns
|
12 |
-
*/
|
13 |
-
export async function convertImageTo(imgBase64: string = "", outputFormat: ImageFileExt): Promise<string> {
|
14 |
-
const format = outputFormat.trim().toLowerCase() as ImageFileExt
|
15 |
-
if (!["jpeg", "jpg", "png", "webp"].includes(format)) {
|
16 |
-
throw new Error(`unsupported file format "${format}"`)
|
17 |
-
}
|
18 |
-
|
19 |
-
const isJpeg = format === "jpg" || format === "jpeg"
|
20 |
-
|
21 |
-
|
22 |
-
if (isJpeg) {
|
23 |
-
return convertImageToJpeg(imgBase64)
|
24 |
-
}
|
25 |
-
|
26 |
-
if (format === "webp") {
|
27 |
-
return convertImageToWebp(imgBase64)
|
28 |
-
}
|
29 |
-
|
30 |
-
return convertImageToPng(imgBase64)
|
31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/convertImageToJpeg.mts
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
import sharp from "sharp"
|
2 |
-
|
3 |
-
export async function convertImageToJpeg(imgBase64: string = "", quality: number = 92): Promise<string> {
|
4 |
-
|
5 |
-
const base64WithoutHeader = imgBase64.split(";base64,")[1] || ""
|
6 |
-
|
7 |
-
if (!base64WithoutHeader) {
|
8 |
-
const slice = `${imgBase64 || ""}`.slice(0, 50)
|
9 |
-
throw new Error(`couldn't process input image "${slice}..."`)
|
10 |
-
}
|
11 |
-
|
12 |
-
// Convert base64 to buffer
|
13 |
-
const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64')
|
14 |
-
|
15 |
-
// Resize the buffer to the target size
|
16 |
-
const newBuffer = await sharp(tmpBuffer)
|
17 |
-
.jpeg({
|
18 |
-
quality,
|
19 |
-
// we don't use progressive: true because we pre-load images anyway
|
20 |
-
})
|
21 |
-
.toBuffer()
|
22 |
-
|
23 |
-
// Convert the buffer back to base64
|
24 |
-
const newImageBase64 = newBuffer.toString('base64')
|
25 |
-
|
26 |
-
return `data:image/jpeg;base64,${newImageBase64}`
|
27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/convertImageToOriginal.mts
DELETED
@@ -1,6 +0,0 @@
|
|
1 |
-
|
2 |
-
// you are reading it right: this function does.. nothing!
|
3 |
-
// it is a NOOP conversion function
|
4 |
-
export async function convertImageToOriginal(imgBase64: string = ""): Promise<string> {
|
5 |
-
return imgBase64
|
6 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/convertImageToPng.mts
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
import sharp from "sharp"
|
2 |
-
|
3 |
-
export async function convertImageToPng(imgBase64: string = ""): Promise<string> {
|
4 |
-
|
5 |
-
const base64WithoutHeader = imgBase64.split(";base64,")[1] || ""
|
6 |
-
|
7 |
-
if (!base64WithoutHeader) {
|
8 |
-
const slice = `${imgBase64 || ""}`.slice(0, 50)
|
9 |
-
throw new Error(`couldn't process input image "${slice}..."`)
|
10 |
-
}
|
11 |
-
|
12 |
-
// Convert base64 to buffer
|
13 |
-
const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64')
|
14 |
-
|
15 |
-
const newBuffer = await sharp(tmpBuffer)
|
16 |
-
.png()
|
17 |
-
.toBuffer()
|
18 |
-
|
19 |
-
// Convert the buffer back to base64
|
20 |
-
const newImageBase64 = newBuffer.toString('base64')
|
21 |
-
|
22 |
-
return `data:image/png;base64,${newImageBase64}`
|
23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/convertImageToWebp.mts
DELETED
@@ -1,41 +0,0 @@
|
|
1 |
-
import sharp from "sharp"
|
2 |
-
|
3 |
-
export async function convertImageToWebp(imgBase64: string = ""): Promise<string> {
|
4 |
-
|
5 |
-
const base64WithoutHeader = imgBase64.split(";base64,")[1] || ""
|
6 |
-
|
7 |
-
if (!base64WithoutHeader) {
|
8 |
-
const slice = `${imgBase64 || ""}`.slice(0, 50)
|
9 |
-
throw new Error(`couldn't process input image "${slice}..."`)
|
10 |
-
}
|
11 |
-
|
12 |
-
// Convert base64 to buffer
|
13 |
-
const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64')
|
14 |
-
|
15 |
-
// Resize the buffer to the target size
|
16 |
-
const newBuffer = await sharp(tmpBuffer)
|
17 |
-
.webp({
|
18 |
-
// for options please see https://sharp.pixelplumbing.com/api-output#webp
|
19 |
-
|
20 |
-
// preset: "photo",
|
21 |
-
|
22 |
-
// effort: 3,
|
23 |
-
|
24 |
-
// for a PNG-like quality
|
25 |
-
// lossless: true,
|
26 |
-
|
27 |
-
// by default it is quality 80
|
28 |
-
quality: 80,
|
29 |
-
|
30 |
-
// nearLossless: true,
|
31 |
-
|
32 |
-
// use high quality chroma subsampling
|
33 |
-
smartSubsample: true,
|
34 |
-
})
|
35 |
-
.toBuffer()
|
36 |
-
|
37 |
-
// Convert the buffer back to base64
|
38 |
-
const newImageBase64 = newBuffer.toString('base64')
|
39 |
-
|
40 |
-
return `data:image/webp;base64,${newImageBase64}`
|
41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/converters/imageFormats.mts
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
export type ImageFileExt = "png" | "jpeg" | "jpg" | "webp"
|
|
|
|
src/core/exporters/{clapWithStoryboardsToVideoFile.mts β clapWithStoryboardsToVideoFile.ts}
RENAMED
@@ -1,7 +1,7 @@
|
|
1 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
|
|
2 |
|
3 |
-
import {
|
4 |
-
import { storyboardSegmentToVideoFile } from "./storyboardSegmentToVideoFile.mts"
|
5 |
|
6 |
export async function clapWithStoryboardsToVideoFile({
|
7 |
clap,
|
|
|
1 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
2 |
+
import { getRandomDirectory } from "@aitube/io"
|
3 |
|
4 |
+
import { storyboardSegmentToVideoFile } from "./storyboardSegmentToVideoFile"
|
|
|
5 |
|
6 |
export async function clapWithStoryboardsToVideoFile({
|
7 |
clap,
|
src/core/exporters/{clapWithVideosToVideoFile.mts β clapWithVideosToVideoFile.ts}
RENAMED
@@ -1,8 +1,7 @@
|
|
1 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
|
|
2 |
|
3 |
-
import {
|
4 |
-
import { videoSegmentToVideoFile } from "./videoSegmentToVideoFile.mts"
|
5 |
-
|
6 |
|
7 |
export async function clapWithVideosToVideoFile({
|
8 |
clap,
|
|
|
1 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
2 |
+
import { getRandomDirectory } from "@aitube/io"
|
3 |
|
4 |
+
import { videoSegmentToVideoFile } from "./videoSegmentToVideoFile"
|
|
|
|
|
5 |
|
6 |
export async function clapWithVideosToVideoFile({
|
7 |
clap,
|
src/core/exporters/{storyboardSegmentToVideoFile.mts β storyboardSegmentToVideoFile.ts}
RENAMED
@@ -1,14 +1,12 @@
|
|
1 |
import { join } from "node:path"
|
2 |
|
3 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
import {
|
6 |
-
import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"
|
7 |
-
import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts"
|
8 |
-
import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts"
|
9 |
-
import { deleteFile } from "../files/deleteFile.mts"
|
10 |
-
import { extractBase64 } from "../base64/extractBase64.mts"
|
11 |
-
import { imageToVideoBase64 } from "../ffmpeg/imageToVideoBase64.mts"
|
12 |
|
13 |
export async function storyboardSegmentToVideoFile({
|
14 |
clap,
|
|
|
1 |
import { join } from "node:path"
|
2 |
|
3 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
4 |
+
import { extractBase64 } from "@aitube/encoders"
|
5 |
+
import { deleteFile, writeBase64ToFile } from "@aitube/io"
|
6 |
+
//import { addTextToVideo, concatenateVideosWithAudio, imageToVideoBase64 } from "@aitube/ffmpeg"
|
7 |
+
import { addTextToVideo, concatenateVideosWithAudio, imageToVideoBase64 } from "../../bug-in-bun/aitube_ffmpeg"
|
8 |
|
9 |
+
import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2"
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
export async function storyboardSegmentToVideoFile({
|
12 |
clap,
|
src/core/exporters/{videoSegmentToVideoFile.mts β videoSegmentToVideoFile.ts}
RENAMED
@@ -1,14 +1,12 @@
|
|
1 |
import { join } from "node:path"
|
2 |
|
3 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
import {
|
6 |
-
import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"
|
7 |
-
import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts"
|
8 |
-
import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts"
|
9 |
-
import { deleteFile } from "../files/deleteFile.mts"
|
10 |
-
import { extractBase64 } from "../base64/extractBase64.mts"
|
11 |
-
|
12 |
|
13 |
export async function videoSegmentToVideoFile({
|
14 |
clap,
|
|
|
1 |
import { join } from "node:path"
|
2 |
|
3 |
import { ClapProject, ClapSegment } from "@aitube/clap"
|
4 |
+
import { extractBase64 } from "@aitube/encoders"
|
5 |
+
import { deleteFile, writeBase64ToFile } from "@aitube/io"
|
6 |
+
// import { addTextToVideo, concatenateVideosWithAudio } from "@aitube/ffmpeg"
|
7 |
+
import { addTextToVideo, concatenateVideosWithAudio } from "../../bug-in-bun/aitube_ffmpeg"
|
8 |
|
9 |
+
import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2"
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
export async function videoSegmentToVideoFile({
|
12 |
clap,
|
src/core/files/deleteFile.mts
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
import { unlink, rm } from "node:fs/promises"
|
2 |
-
|
3 |
-
export async function deleteFile(filePath: string, debug?: boolean): Promise<boolean> {
|
4 |
-
try {
|
5 |
-
await rm(filePath, { recursive: true, force: true })
|
6 |
-
// await unlink(filePath)
|
7 |
-
return true
|
8 |
-
} catch (err) {
|
9 |
-
if (debug) {
|
10 |
-
console.error(`failed to unlink file at ${filePath}: ${err}`)
|
11 |
-
}
|
12 |
-
}
|
13 |
-
return false
|
14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/files/deleteFileWithName.mts
DELETED
@@ -1,12 +0,0 @@
|
|
1 |
-
import { promises as fs } from "node:fs"
|
2 |
-
import path from "node:path"
|
3 |
-
import { deleteFile } from "./deleteFile.mts"
|
4 |
-
|
5 |
-
export const deleteFilesWithName = async (dir: string, name: string, debug?: boolean) => {
|
6 |
-
console.log(`deleteFilesWithName(${dir}, ${name})`)
|
7 |
-
for (const file of await fs.readdir(dir)) {
|
8 |
-
if (file.includes(name)) {
|
9 |
-
await deleteFile(path.join(dir, file))
|
10 |
-
}
|
11 |
-
}
|
12 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|