diff --git a/Dockerfile b/Dockerfile index 8e81170c9a57eea7aebd31b778c5f945a2ec00d4..063c821dfbeea7ab37d616b6c7d611c3b02c3f55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,9 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apk update +# for dev mode +RUN apk add git git-lfs get procps htop vim nano + RUN apk add alpine-sdk pkgconfig # For FFMPEG and gl concat @@ -16,32 +19,31 @@ RUN apk add build-base gcompat udev ttf-opensans chromium RUN apk add ffmpeg # Set up a new user named "user" with user ID 1000 -RUN adduser --disabled-password --uid 1001 user +RUN adduser --disabled-password --uid 1000 user # Switch to the "user" user USER user # Set home to the user's home directory -ENV HOME=/home/user \ - PATH=/home/user/.local/bin:$PATH +ENV PATH=.local/bin:$PATH # Set the working directory to the user's home directory -WORKDIR $HOME/app +WORKDIR /app # Install app dependencies # A wildcard is used to ensure both package.json AND package-lock.json are copied # where available (npm@5+) -COPY --chown=user package*.json $HOME/app +COPYY --link --chown=user package*.json /app # make sure the .env is copied as well -COPY --chown=user .env $HOME/app +COPY --link --chown=user .env /app RUN ffmpeg -version -RUN npm install +# Copy the current directory contents into the container at /app setting the owner to the user +COPY --link --chown=user . $HOME/app -# Copy the current directory contents into the container at $HOME/app setting the owner to the user -COPY --chown=user . $HOME/app +RUN npm ci EXPOSE 7860 diff --git a/package-lock.json b/package-lock.json index 3d4fcd4eb8f03c12be96fa8c530e257e1594e509..eab4f028b92502894dbb4b6e6f921aff30933e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,27 +10,20 @@ "license": "Apache License", "dependencies": { "@aitube/clap": "0.0.7", + "@aitube/encoders": "0.0.0", + "@aitube/io": "0.0.0", "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.24", "@types/uuid": "^9.0.2", "dotenv": "^16.3.1", - "eventsource-parser": "^1.0.0", "express": "^4.18.2", "fluent-ffmpeg": "^2.1.2", - "fs-extra": "^11.1.1", - "mime-types": "^2.1.35", - "node-fetch": "^3.3.1", - "puppeteer": "^22.7.0", + "puppeteer": "^22.7.1", "query-string": "^9.0.0", "sharp": "^0.33.3", - "temp-dir": "^3.0.0", - "ts-node": "^10.9.1", - "type-fest": "^4.8.2", - "uuid": "^9.0.0", - "yaml": "^2.4.1" + "ts-node": "^10.9.1" }, "devDependencies": { - "@types/mime-types": "^2.1.4", "@types/node": "^20.12.7", "tsx": "^4.7.0" } @@ -47,6 +40,24 @@ "typescript": "^5.4.5" } }, + "node_modules/@aitube/encoders": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@aitube/encoders/-/encoders-0.0.0.tgz", + "integrity": "sha512-jKIii0m0Lwr6l+slA9cPg/c8WCl2uylNYON74VU+3cgi//7Yt34Ym1afGIjQd2vEWzPR7LrlHcrb4F0War7Fmg==", + "peerDependencies": { + "typescript": "^5.4.5" + } + }, + "node_modules/@aitube/io": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@aitube/io/-/io-0.0.0.tgz", + "integrity": "sha512-ay/h4VZGmlS3ZHraHHLkeXovXEv6WU2SSZp+n27rgasnoDsAU8If2IXgZXxIjnpqOigZ8qed/GhmCzvhqvmOrQ==", + "dependencies": { + "mime-types": "^2.1.35", + "sharp": "^0.33.3", + "uuid": "^9.0.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", @@ -60,19 +71,19 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1046,12 +1057,6 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "dev": true - }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -1529,11 +1534,11 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "engines": { - "node": ">= 12" + "node": ">= 14" } }, "node_modules/debug": { @@ -1806,14 +1811,6 @@ "node": ">= 0.6" } }, - "node_modules/eventsource-parser": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", - "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", - "engines": { - "node": ">=14.18" - } - }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -1908,28 +1905,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/filter-obj": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", @@ -1970,17 +1945,6 @@ "node": ">=0.8.0" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2098,14 +2062,6 @@ "node": ">= 14" } }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "engines": { - "node": ">= 14" - } - }, "node_modules/get-uri/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2496,41 +2452,6 @@ "node": ">= 0.4.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -3217,14 +3138,6 @@ "streamx": "^2.15.0" } }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "engines": { - "node": ">=14.16" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -3304,17 +3217,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-fest": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.0.tgz", - "integrity": "sha512-+dbmiyliDY/2TTcjCS7NpI9yV2iEFlUDk5TKnsbkN7ZoRu5s7bT+zvYtNFhFXC2oLwURGT2frACAZvbbyNBI+w==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3408,14 +3310,6 @@ "node": ">= 0.8" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 16386a4d51c132ca30d8dd5c491b5a18b508e881..aa78aeaa00f15308743c91c0cd4163656d797c2f 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,33 @@ { "name": "ai-tube-clap-exporter", "version": "1.0.0", - "description": "A service to convert a .clap (will all its assets) to a video file", - "main": "src/index.mts", + "description": "API service to convert a .clap (will all its assets) to a video file", + "main": "src/index.ts", "scripts": { - "start": "tsx src/index.mts", - "dev": "tsx src/index.mts", + "start": "tsx src/index.ts", + "dev": "tsx src/index.ts", "docker": "npm run docker:build && npm run docker:run", - "docker:build": "docker build -t ai-tube-robot .", - "docker:run": "docker run -it -p 7860:7860 ai-tube-robot", - "alchemy:test": "tsx src/core/alchemy/test.mts" + "docker:build": "docker build -t ai-tube-clap-exporter .", + "docker:run": "docker run -it -p 7860:7860 ai-tube-clap-exporter" }, "author": "Julian Bilcke ", "license": "Apache License", "dependencies": { "@aitube/clap": "0.0.7", + "@aitube/encoders": "0.0.0", + "@aitube/io": "0.0.0", "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.24", "@types/uuid": "^9.0.2", "dotenv": "^16.3.1", - "eventsource-parser": "^1.0.0", "express": "^4.18.2", "fluent-ffmpeg": "^2.1.2", - "fs-extra": "^11.1.1", - "mime-types": "^2.1.35", - "node-fetch": "^3.3.1", - "puppeteer": "^22.7.0", + "puppeteer": "^22.7.1", "query-string": "^9.0.0", "sharp": "^0.33.3", - "temp-dir": "^3.0.0", - "ts-node": "^10.9.1", - "type-fest": "^4.8.2", - "uuid": "^9.0.0", - "yaml": "^2.4.1" + "ts-node": "^10.9.1" }, "devDependencies": { - "@types/mime-types": "^2.1.4", "@types/node": "^20.12.7", "tsx": "^4.7.0" } diff --git a/src/bug-in-bun/FIXME.md b/src/bug-in-bun/FIXME.md new file mode 100644 index 0000000000000000000000000000000000000000..64d048d1837b24121c294608e2fad11d6cb9a50d --- /dev/null +++ b/src/bug-in-bun/FIXME.md @@ -0,0 +1,10 @@ +We should use import .... from "@aitube/ffmpeg" + +But there is a bug with Bun: + +https://github.com/oven-sh/bun/issues/4477 + + +Once fixed, we can delete aitube_ffmpeg (and the dependencies like fluent-ffmpeg, puppeteer etc) + +and only use @aitube/ffmpeg \ No newline at end of file diff --git a/src/core/ffmpeg/getMediaInfo.mts b/src/bug-in-bun/aitube_ffmpeg/analyze/getMediaInfo.ts similarity index 89% rename from src/core/ffmpeg/getMediaInfo.mts rename to src/bug-in-bun/aitube_ffmpeg/analyze/getMediaInfo.ts index 3c3985c65753f440fbb2423aa02ce41dd8f5b588..ee3e31d9abdf926c07e0d04c0fb204a8a4aca4d2 100644 --- a/src/core/ffmpeg/getMediaInfo.mts +++ b/src/bug-in-bun/aitube_ffmpeg/analyze/getMediaInfo.ts @@ -1,8 +1,8 @@ -import ffmpeg from "fluent-ffmpeg"; +import { tmpdir } from "node:os" +import { writeFile, rm } from "node:fs/promises" +import { join } from "node:path" -import { tmpdir } from "node:os"; -import { promises as fs } from "node:fs"; -import { join } from "node:path"; +import ffmpeg from "fluent-ffmpeg" export type MediaMetadata = { durationInSec: number; @@ -31,13 +31,13 @@ export async function getMediaInfo(input: string): Promise { const tempFileName = join(tmpdir(), `temp-media-${Date.now()}`); // Write the buffer to a temporary file - await fs.writeFile(tempFileName, buffer); + await writeFile(tempFileName, buffer); // Get metadata from the temporary file then delete the file try { return await getMetaDataFromPath(tempFileName); } finally { - await fs.unlink(tempFileName); + await rm(tempFileName); } } diff --git a/src/bug-in-bun/aitube_ffmpeg/analyze/index.ts b/src/bug-in-bun/aitube_ffmpeg/analyze/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1a34b1b63dfdd2bd8062b02d01422db203cd0e3 --- /dev/null +++ b/src/bug-in-bun/aitube_ffmpeg/analyze/index.ts @@ -0,0 +1 @@ +export { getMediaInfo } from "./getMediaInfo" \ No newline at end of file diff --git a/src/core/ffmpeg/concatenateAudio.mts b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateAudio.ts similarity index 82% rename from src/core/ffmpeg/concatenateAudio.mts rename to src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateAudio.ts index c073aee15c79a707534973e1493e4c31852600f2..8a75cb4d13e67d787008eb2e1f59a6b5f1d1f070 100644 --- a/src/core/ffmpeg/concatenateAudio.mts +++ b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateAudio.ts @@ -1,13 +1,12 @@ -import { existsSync, promises as fs } from "node:fs" -import os from "node:os" +import { existsSync } from "node:fs" import path from "node:path" -import { v4 as uuidv4 } from "uuid"; -import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; -import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"; -import { getMediaInfo } from "./getMediaInfo.mts"; -import { removeTemporaryFiles } from "../files/removeTmpFiles.mts"; -import { addBase64Header } from "../base64/addBase64.mts"; +import { v4 as uuidv4 } from "uuid" +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg" +import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io" +import { addBase64Header } from "@aitube/encoders" + +import { getMediaInfo } from "../analyze/getMediaInfo" export type ConcatenateAudioOptions = { // those are base64 audio strings! @@ -34,20 +33,19 @@ export async function concatenateAudio({ throw new Error("Audios must be provided in an array"); } - const tempDir = path.join(os.tmpdir(), uuidv4()); - await fs.mkdir(tempDir); + const tempDir = await getRandomDirectory() // console.log(" |- created tmp dir") // trivial case: there is only one audio to concatenate! if (audioTracks.length === 1 && audioTracks[0]) { const audioTrack = audioTracks[0] - const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`); - await writeBase64ToFile(addBase64Header(audioTrack, "wav"), outputFilePath); + const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`) + await writeBase64ToFile(addBase64Header(audioTrack, "wav"), outputFilePath) // console.log(" |- there is only one track! so.. returning that") - const { durationInSec } = await getMediaInfo(outputFilePath); - return { filepath: outputFilePath, durationInSec }; + const { durationInSec } = await getMediaInfo(outputFilePath) + return { filepath: outputFilePath, durationInSec } } if (audioFilePaths.length === 1) { @@ -60,10 +58,12 @@ export async function concatenateAudio({ for (const track of audioTracks) { if (!track) { continue } const audioFilePath = path.join(tempDir, `audio_${++i}.wav`); - await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath); + await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath) + audioFilePaths.push(audioFilePath); } + // TODO: convert this to an async filter using promises audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio)) const outputFilePath = output ?? path.join(tempDir, `${uuidv4()}.${outputFormat}`); @@ -77,7 +77,7 @@ export async function concatenateAudio({ prevLabel = nextLabel; } - + /* console.log(" |- concatenateAudio(): DEBUG:", { tempDir, audioFilePaths, @@ -85,13 +85,13 @@ export async function concatenateAudio({ filterComplex, prevLabel }) + */ let cmd: FfmpegCommand = ffmpeg() // .outputOptions('-vn'); audioFilePaths.forEach((audio, i) => { - cmd = cmd.input(audio); - }); - + cmd = cmd.input(audio) + }) const promise = new Promise((resolve, reject) => { cmd = cmd @@ -99,10 +99,12 @@ export async function concatenateAudio({ .on('end', async () => { try { const { durationInSec } = await getMediaInfo(outputFilePath); + // console.log("concatenation ended! see ->", outputFilePath) - resolve({ filepath: outputFilePath, durationInSec }); + resolve({ filepath: outputFilePath, durationInSec }) + } catch (err) { - reject(err); + reject(err) } }) .complexFilter(filterComplex, prevLabel) diff --git a/src/core/ffmpeg/concatenateVideos.mts b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideos.ts similarity index 71% rename from src/core/ffmpeg/concatenateVideos.mts rename to src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideos.ts index b1408b5e816e1ed4f426fb8f037eb290ff7907a3..dea2646cfa4db78121c8e240cb4b4f445b09b728 100644 --- a/src/core/ffmpeg/concatenateVideos.mts +++ b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideos.ts @@ -1,11 +1,11 @@ -import { existsSync, promises as fs } from "node:fs"; -import os from "node:os"; -import path from "node:path"; +import { existsSync } from "node:fs" +import path from "node:path" -import { v4 as uuidv4 } from "uuid"; -import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; +import { v4 as uuidv4 } from "uuid" +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg" +import { getRandomDirectory } from "@aitube/io" -import { getMediaInfo } from "./getMediaInfo.mts"; +import { getMediaInfo } from "../analyze/getMediaInfo" export type ConcatenateVideoOutput = { filepath: string; @@ -22,22 +22,21 @@ export async function concatenateVideos({ videoFilePaths: string[]; }): Promise { if (!Array.isArray(videoFilePaths)) { - throw new Error("Videos must be provided in an array"); + throw new Error("Videos must be provided in an array") } videoFilePaths = videoFilePaths.filter((videoPath) => existsSync(videoPath)) // Create a temporary working directory - const tempDir = path.join(os.tmpdir(), uuidv4()); - await fs.mkdir(tempDir); + const tempDir = await getRandomDirectory() - const filePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`); + const filePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`) if (!filePath) { - throw new Error("Failed to generate a valid temporary file path"); + throw new Error("Failed to generate a valid temporary file path") } - let cmd: FfmpegCommand = ffmpeg(); + let cmd: FfmpegCommand = ffmpeg() videoFilePaths.forEach((video) => { cmd = cmd.addInput(video) @@ -57,5 +56,5 @@ export async function concatenateVideos({ }) .mergeToFile(filePath, tempDir); } - ); -}; + ) +} diff --git a/src/core/ffmpeg/concatenateVideosAndMergeAudio.mts b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosAndMergeAudio.ts similarity index 87% rename from src/core/ffmpeg/concatenateVideosAndMergeAudio.mts rename to src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosAndMergeAudio.ts index 23284d4d8c54ee22f402fc5543a6ca8fb12b5165..5a419943a071cac2366c5c7e0ba5756551a8ea7c 100644 --- a/src/core/ffmpeg/concatenateVideosAndMergeAudio.mts +++ b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosAndMergeAudio.ts @@ -1,15 +1,13 @@ -import { existsSync, promises as fs } from "node:fs" -import os from "node:os" +import { existsSync } from "node:fs" import path from "node:path" -import { v4 as uuidv4 } from "uuid"; -import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; -import { concatenateVideos } from "./concatenateVideos.mts"; -import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"; -import { getMediaInfo } from "./getMediaInfo.mts"; -import { removeTemporaryFiles } from "../files/removeTmpFiles.mts"; -import { addBase64Header } from "../base64/addBase64.mts"; -import { extractBase64 } from "../base64/extractBase64.mts"; +import { v4 as uuidv4 } from "uuid" +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg" +import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io" +import { addBase64Header, extractBase64 } from "@aitube/encoders" + +import { getMediaInfo } from "../analyze/getMediaInfo" +import { concatenateVideos } from "./concatenateVideos" type ConcatenateVideoAndMergeAudioOptions = { output?: string; @@ -36,8 +34,7 @@ export const concatenateVideosAndMergeAudio = async ({ try { // Prepare temporary directories - const tempDir = path.join(os.tmpdir(), uuidv4()); - await fs.mkdir(tempDir); + const tempDir = await getRandomDirectory() let i = 0 for (const audioTrack of audioTracks) { diff --git a/src/core/ffmpeg/concatenateVideosWithAudio.mts b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosWithAudio.ts similarity index 88% rename from src/core/ffmpeg/concatenateVideosWithAudio.mts rename to src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosWithAudio.ts index ac6392a9a5d144dd578302e55d7bb3ac5aff459d..409d867881a4eb1916cdf7d2a8a066cddd896ece 100644 --- a/src/core/ffmpeg/concatenateVideosWithAudio.mts +++ b/src/bug-in-bun/aitube_ffmpeg/concatenate/concatenateVideosWithAudio.ts @@ -1,15 +1,14 @@ -import { existsSync, promises as fs } from "node:fs" -import os from "node:os" +import { existsSync } from "node:fs" +import { readFile } from "node:fs/promises" import path from "node:path" -import { v4 as uuidv4 } from "uuid"; -import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"; -import { concatenateVideos } from "./concatenateVideos.mts"; -import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"; -import { getMediaInfo } from "./getMediaInfo.mts"; -import { removeTemporaryFiles } from "../files/removeTmpFiles.mts"; -import { addBase64Header } from "../base64/addBase64.mts"; -import { extractBase64 } from "../base64/extractBase64.mts"; +import { v4 as uuidv4 } from "uuid" +import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg" +import { addBase64Header, extractBase64 } from "@aitube/encoders" +import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io" + +import { getMediaInfo } from "../analyze/getMediaInfo" +import { concatenateVideos } from "./concatenateVideos" export type SupportedExportFormat = "mp4" | "webm" export const defaultExportFormat = "mp4" @@ -41,14 +40,14 @@ export const concatenateVideosWithAudio = async ({ try { // Prepare temporary directories - const tempDir = path.join(os.tmpdir(), uuidv4()); - await fs.mkdir(tempDir); + const tempDir = await getRandomDirectory() if (audioTrack && audioTrack.length > 0) { const analysis = extractBase64(audioTrack) // console.log(`concatenateVideosWithAudio: writing down an audio file (${analysis.extension}) from the supplied base64 track`) - audioFilePath = path.join(tempDir, `audio.${analysis.extension}`); - await writeBase64ToFile(addBase64Header(audioTrack, analysis.extension), audioFilePath); + audioFilePath = path.join(tempDir, `audio.${analysis.extension}`) + + await writeBase64ToFile(addBase64Header(audioTrack, analysis.extension), audioFilePath) } // Decode and concatenate base64 video tracks to temporary file @@ -58,13 +57,13 @@ export const concatenateVideosWithAudio = async ({ // note: here we assume the input video is in mp4 const analysis = extractBase64(audioTrack) - const videoFilePath = path.join(tempDir, `video${++i}.${analysis.extension}`); + const videoFilePath = path.join(tempDir, `video${++i}.${analysis.extension}`) // console.log(`concatenateVideosWithAudio: writing down a video file (${analysis.extension}) from the supplied base64 track`) - await writeBase64ToFile(addBase64Header(track, analysis.extension), videoFilePath); + await writeBase64ToFile(addBase64Header(track, analysis.extension), videoFilePath) - videoFilePaths.push(videoFilePath); + videoFilePaths.push(videoFilePath) } videoFilePaths = videoFilePaths.filter((video) => existsSync(video)) @@ -162,7 +161,7 @@ export const concatenateVideosWithAudio = async ({ try { if (asBase64) { try { - const outputBuffer = await fs.readFile(finalOutputFilePath); + const outputBuffer = await readFile(finalOutputFilePath); const outputBase64 = addBase64Header(outputBuffer.toString("base64"), format) resolve(outputBase64); } catch (error) { diff --git a/src/core/ffmpeg/createVideoFromFrames.mts b/src/bug-in-bun/aitube_ffmpeg/concatenate/createVideoFromFrames.ts similarity index 96% rename from src/core/ffmpeg/createVideoFromFrames.mts rename to src/bug-in-bun/aitube_ffmpeg/concatenate/createVideoFromFrames.ts index d0405df8b4e8eed6ff2fef4047071e6cfaed8950..d52ff92f616a7f659ff1424297ad66c57b9194b7 100644 --- a/src/core/ffmpeg/createVideoFromFrames.mts +++ b/src/bug-in-bun/aitube_ffmpeg/concatenate/createVideoFromFrames.ts @@ -5,7 +5,7 @@ import path from "node:path" import ffmpeg from "fluent-ffmpeg" import { v4 as uuidv4 } from "uuid" -import { getMediaInfo } from "./getMediaInfo.mts" +import { getMediaInfo } from "../analyze/getMediaInfo" export async function createVideoFromFrames({ inputFramesDirectory, @@ -19,7 +19,7 @@ export async function createVideoFromFrames({ // 3. grain has too much entropy and cannot be compressed, so it multiplies by 5 the size weight grainAmount = 0, // Optional parameter for film grain (eg. 10) - inputVideoToUseAsAudio, // Optional parameter for audio input (need to be a mp4, but it can be a base64 data URI or a file path) + inputVideoToUseAsAudio = "", // Optional parameter for audio input (need to be a mp4, but it can be a base64 data URI or a file path) debug = false, @@ -42,7 +42,7 @@ export async function createVideoFromFrames({ // Construct the input frame pattern - const inputFramePattern = path.join(inputFramesDirectory, framesFilePattern); + const inputFramePattern = path.join(inputFramesDirectory, framesFilePattern || ""); // Create a temporary working directory diff --git a/src/bug-in-bun/aitube_ffmpeg/concatenate/index.ts b/src/bug-in-bun/aitube_ffmpeg/concatenate/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..464ad55d7eb9e33effdd27c4fa2de2cfaef5858e --- /dev/null +++ b/src/bug-in-bun/aitube_ffmpeg/concatenate/index.ts @@ -0,0 +1,8 @@ +export { concatenateAudio } from "./concatenateAudio" +export type { ConcatenateAudioOutput } from "./concatenateAudio" +export { concatenateVideos } from "./concatenateVideos" +export { concatenateVideosAndMergeAudio } from "./concatenateVideosAndMergeAudio" +export { concatenateVideosWithAudio } from "./concatenateVideosWithAudio" +export { defaultExportFormat } from "./concatenateVideosWithAudio" +export type { SupportedExportFormat } from "./concatenateVideosWithAudio" +export { createVideoFromFrames } from "./createVideoFromFrames" \ No newline at end of file diff --git a/src/core/ffmpeg/convertAudioToWav.mts b/src/bug-in-bun/aitube_ffmpeg/convert/convertAudioToWav.ts similarity index 76% rename from src/core/ffmpeg/convertAudioToWav.mts rename to src/bug-in-bun/aitube_ffmpeg/convert/convertAudioToWav.ts index 17602abe187cdef6a84cbd5b391f7af9d4895105..3c924d5ce159d8b91c57986d751e06869b55c568 100644 --- a/src/core/ffmpeg/convertAudioToWav.mts +++ b/src/bug-in-bun/aitube_ffmpeg/convert/convertAudioToWav.ts @@ -1,8 +1,9 @@ -import { promises as fs } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import ffmpeg from "fluent-ffmpeg"; -import { Buffer } from "node:buffer"; +import { mkdtemp, writeFile, readFile, stat } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { Buffer } from "node:buffer" + +import ffmpeg from "fluent-ffmpeg" type ConvertAudioToWavParams = { input: string; @@ -27,21 +28,21 @@ export async function convertAudioToWav({ const inputBuffer = Buffer.from(matches[2], "base64"); const inputFormat = matches[1]; // Either 'mp3' or 'wav' - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-input-")); + const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-input-")); inputAudioPath = path.join(tempDir, `temp.${inputFormat}`); // Write the base64 data to the temporary file - await fs.writeFile(inputAudioPath, inputBuffer); + await writeFile(inputAudioPath, inputBuffer); } else { // Verify that the input file exists - if (!(await fs.stat(inputAudioPath)).isFile()) { + if (!(await stat(inputAudioPath)).isFile()) { throw new Error(`Input audio file does not exist: ${inputAudioPath}`); } } // If no output path is provided, create a temporary file for the output if (!outputAudioPath) { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-output-")); + const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-output-")); outputAudioPath = path.join(tempDir, `${path.parse(inputAudioPath).name}.wav`); } @@ -54,7 +55,7 @@ export async function convertAudioToWav({ .on("end", async () => { if (asBase64) { try { - const audioBuffer = await fs.readFile(outputAudioPath); + const audioBuffer = await readFile(outputAudioPath); const audioBase64 = `data:audio/wav;base64,${audioBuffer.toString("base64")}`; resolve(audioBase64); } catch (error) { diff --git a/src/core/ffmpeg/convertMp4ToMp3.mts b/src/bug-in-bun/aitube_ffmpeg/convert/convertMp4ToMp3.ts similarity index 91% rename from src/core/ffmpeg/convertMp4ToMp3.mts rename to src/bug-in-bun/aitube_ffmpeg/convert/convertMp4ToMp3.ts index 576c6b03114df6236252c783ee919f772391e392..664e3bb36e50a56d6d18ee1528d2a3fbc1cf3610 100644 --- a/src/core/ffmpeg/convertMp4ToMp3.mts +++ b/src/bug-in-bun/aitube_ffmpeg/convert/convertMp4ToMp3.ts @@ -1,10 +1,11 @@ -import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import ffmpeg from "fluent-ffmpeg"; -import { tmpdir } from "node:os"; -import { Buffer } from "node:buffer"; +import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { tmpdir } from "node:os" +import { Buffer } from "node:buffer" + +import ffmpeg from "fluent-ffmpeg" export async function convertMp4ToMp3({ input, diff --git a/src/core/ffmpeg/convertMp4ToWebm.mts b/src/bug-in-bun/aitube_ffmpeg/convert/convertMp4ToWebm.ts similarity index 93% rename from src/core/ffmpeg/convertMp4ToWebm.mts rename to src/bug-in-bun/aitube_ffmpeg/convert/convertMp4ToWebm.ts index b5223ccaa5673b9946f901772af7c492c8ed2ef1..1dff20cd25603ca11b3260123d23bdc74e7953f4 100644 --- a/src/core/ffmpeg/convertMp4ToWebm.mts +++ b/src/bug-in-bun/aitube_ffmpeg/convert/convertMp4ToWebm.ts @@ -1,11 +1,10 @@ -import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises"; -import path from "node:path"; +import { mkdtemp, stat, writeFile, readFile } from "node:fs/promises" +import path from "node:path" +import { tmpdir } from "node:os" +import { Buffer } from "node:buffer" -import { tmpdir } from "node:os"; -import { Buffer } from "node:buffer"; - -import ffmpeg from "fluent-ffmpeg"; +import ffmpeg from "fluent-ffmpeg" export async function convertMp4ToWebm({ input, diff --git a/src/bug-in-bun/aitube_ffmpeg/convert/index.ts b/src/bug-in-bun/aitube_ffmpeg/convert/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..758bc89fc7cde4a81167f4646fd14d3e166b7c9e --- /dev/null +++ b/src/bug-in-bun/aitube_ffmpeg/convert/index.ts @@ -0,0 +1,3 @@ +export { convertAudioToWav } from "./convertAudioToWav" +export { convertMp4ToMp3 } from "./convertMp4ToMp3" +export { convertMp4ToWebm } from "./convertMp4ToWebm" diff --git a/src/bug-in-bun/aitube_ffmpeg/index.ts b/src/bug-in-bun/aitube_ffmpeg/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b210befabe7e776f054ac0b5a2d0c8b36080a20 --- /dev/null +++ b/src/bug-in-bun/aitube_ffmpeg/index.ts @@ -0,0 +1,38 @@ +export { + getMediaInfo +} from "./analyze" + +export { + concatenateAudio, + concatenateVideos, + concatenateVideosAndMergeAudio, + concatenateVideosWithAudio, + defaultExportFormat, + createVideoFromFrames +} from "./concatenate" + +export type { + SupportedExportFormat, + ConcatenateAudioOutput + } from "./concatenate" + +export { + convertAudioToWav, + convertMp4ToMp3, + convertMp4ToWebm, +} from "./convert" + +export { + addImageToVideo, + addTextToVideo, + createTextOverlayImage, + getCssStyle, + htmlToBase64Png, + imageToVideoBase64, +} from "./overlay" + +export { + cropBase64Video, + cropVideo, + scaleVideo, +} from "./transform" diff --git a/src/core/ffmpeg/addImageToVideo.mts b/src/bug-in-bun/aitube_ffmpeg/overlay/addImageToVideo.ts similarity index 82% rename from src/core/ffmpeg/addImageToVideo.mts rename to src/bug-in-bun/aitube_ffmpeg/overlay/addImageToVideo.ts index 1660d098f6de8bfcce392c18c3747ce473987fe8..eb0816c6033fa696e8957d642ac558b2bf46cae3 100644 --- a/src/core/ffmpeg/addImageToVideo.mts +++ b/src/bug-in-bun/aitube_ffmpeg/overlay/addImageToVideo.ts @@ -1,8 +1,9 @@ -import { promises as fs, existsSync } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import ffmpeg from "fluent-ffmpeg"; -import { v4 as uuidv4 } from "uuid"; +import { existsSync } from "node:fs" +import path from "node:path" + +import ffmpeg from "fluent-ffmpeg" +import { v4 as uuidv4 } from "uuid" +import { getRandomDirectory } from "@aitube/io" type AddImageToVideoParams = { inputVideoPath: string; @@ -25,7 +26,7 @@ export async function addImageToVideo({ // If no output path is provided, create a temporary file for output if (!outputVideoPath) { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), uuidv4())); + const tempDir = await getRandomDirectory() outputVideoPath = path.join(tempDir, `${uuidv4()}.mp4`); } diff --git a/src/core/ffmpeg/addTextToVideo.mts b/src/bug-in-bun/aitube_ffmpeg/overlay/addTextToVideo.ts similarity index 89% rename from src/core/ffmpeg/addTextToVideo.mts rename to src/bug-in-bun/aitube_ffmpeg/overlay/addTextToVideo.ts index c6aa0c5d060297fd6f01886f7dcffd6ae085da41..9ce2f1b575427cc8881765ffec7dc237d0f40b42 100644 --- a/src/core/ffmpeg/addTextToVideo.mts +++ b/src/bug-in-bun/aitube_ffmpeg/overlay/addTextToVideo.ts @@ -1,6 +1,7 @@ -import { createTextOverlayImage } from "./createTextOverlayImage.mts" -import { addImageToVideo } from "./addImageToVideo.mts" -import { deleteFile } from "../files/deleteFile.mts" +import { deleteFile } from "@aitube/io" + +import { createTextOverlayImage } from "./createTextOverlayImage" +import { addImageToVideo } from "./addImageToVideo" export async function addTextToVideo({ inputVideoPath, diff --git a/src/core/ffmpeg/createTextOverlayImage.mts b/src/bug-in-bun/aitube_ffmpeg/overlay/createTextOverlayImage.ts similarity index 92% rename from src/core/ffmpeg/createTextOverlayImage.mts rename to src/bug-in-bun/aitube_ffmpeg/overlay/createTextOverlayImage.ts index 5592de772dd32c7a71e1b7aa5ea368c8c0666f71..8dcbc939bcc70e4542125d3b20bd44d36ad3fe74 100644 --- a/src/core/ffmpeg/createTextOverlayImage.mts +++ b/src/bug-in-bun/aitube_ffmpeg/overlay/createTextOverlayImage.ts @@ -1,6 +1,6 @@ -import { TextOverlayFont, TextOverlayFontWeight, TextOverlayPosition, TextOverlayStyle, getCssStyle } from "../utils/getCssStyle.mts" -import { htmlToBase64Png } from "../converters/htmlToBase64Png.mts" +import { TextOverlayFont, TextOverlayFontWeight, TextOverlayPosition, TextOverlayStyle, getCssStyle } from "./getCssStyle" +import { htmlToBase64Png } from "./htmlToBase64Png" // generate a PNG overlay using HTML // most sizes are in percentage of the image height diff --git a/src/core/utils/getCssStyle.mts b/src/bug-in-bun/aitube_ffmpeg/overlay/getCssStyle.ts similarity index 100% rename from src/core/utils/getCssStyle.mts rename to src/bug-in-bun/aitube_ffmpeg/overlay/getCssStyle.ts diff --git a/src/core/converters/htmlToBase64Png.mts b/src/bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64Png.ts similarity index 95% rename from src/core/converters/htmlToBase64Png.mts rename to src/bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64Png.ts index be396081dc739e8560d31e6844a084a672a56c51..fcb4662f8b6b3b73ac6d5d91f15be14cc24543df 100644 --- a/src/core/converters/htmlToBase64Png.mts +++ b/src/bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64Png.ts @@ -7,7 +7,7 @@ import puppeteer from "puppeteer" export async function htmlToBase64Png({ outputImagePath, - html, + html = "", width = 800, height = 600, }: { @@ -53,6 +53,8 @@ export async function htmlToBase64Png({ const content = await page.$("body") + if (!content) { throw new Error (`coudln't find body content`) } + const buffer = await content.screenshot({ path: outputImagePath, omitBackground: true, diff --git a/src/core/ffmpeg/imageToVideoBase64.mts b/src/bug-in-bun/aitube_ffmpeg/overlay/imageToVideoBase64.ts similarity index 80% rename from src/core/ffmpeg/imageToVideoBase64.mts rename to src/bug-in-bun/aitube_ffmpeg/overlay/imageToVideoBase64.ts index d61fb7087c742c7cb29f9c4fc2ae9219a3daba7c..34e8672bc72447bb3cb5d126e4b43f20ec0197e7 100644 --- a/src/core/ffmpeg/imageToVideoBase64.mts +++ b/src/bug-in-bun/aitube_ffmpeg/overlay/imageToVideoBase64.ts @@ -1,8 +1,8 @@ -import { rm, mkdir, writeFile, readFile } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import ffmpeg from "fluent-ffmpeg"; -import { getRandomDirectory } from "../files/getRandomDirectory.mts"; +import { rm, writeFile, readFile } from "node:fs/promises" +import path from "node:path" + +import ffmpeg from "fluent-ffmpeg" +import { getRandomDirectory } from "@aitube/io" /** * Converts an image in Base64 format to a video encoded in Base64. @@ -82,13 +82,21 @@ export async function imageToVideoBase64({ '-pix_fmt yuv420p' ]) - // Apply zoompan filter only if zoom rate is greater than 0 - if (zoomInRatePerSecond > 0) { - const totalZoomFactor = 1 + (zoomInRatePerSecond / 100) * durationInSeconds; - ffmpegCommand = ffmpegCommand.videoFilters(`zoompan=z='min(zoom+${zoomInRatePerSecond}/100,zoom*${totalZoomFactor})':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1`); - } - + if (zoomInRatePerSecond > 0) { + const zoomIncreasePerSecond = zoomInRatePerSecond / 100; + const totalZoomFactor = 1 + (zoomIncreasePerSecond * durationInSeconds); + const framesTotal = durationInSeconds * fps; + const zoomPerFrame = zoomIncreasePerSecond / fps; + + const zoomFormula = `if(lte(zoom\\,${totalZoomFactor}),zoom+${zoomPerFrame}\\,zoom)`; + + ffmpegCommand = ffmpegCommand.videoFilters(`zoompan=z='${zoomFormula}':d=${framesTotal}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)'`); + } + return ffmpegCommand + .on('start', function(commandLine) { + console.log('imageToVideoBase64: Spawned Ffmpeg with command: ' + commandLine); + }) .on('end', () => resolve()) .on('error', (err) => reject(err)) .save(outputFilePath); diff --git a/src/bug-in-bun/aitube_ffmpeg/overlay/index.ts b/src/bug-in-bun/aitube_ffmpeg/overlay/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4340d87d3537865c41d69c106aadb20461af02d5 --- /dev/null +++ b/src/bug-in-bun/aitube_ffmpeg/overlay/index.ts @@ -0,0 +1,6 @@ +export { addImageToVideo } from "./addImageToVideo" +export { addTextToVideo } from "./addTextToVideo" +export { createTextOverlayImage } from "./createTextOverlayImage" +export { getCssStyle } from "./getCssStyle" +export { htmlToBase64Png } from "./htmlToBase64Png" +export { imageToVideoBase64 } from "./imageToVideoBase64" diff --git a/src/core/ffmpeg/cropBase64Video.mts b/src/bug-in-bun/aitube_ffmpeg/transform/cropBase64Video.ts similarity index 88% rename from src/core/ffmpeg/cropBase64Video.mts rename to src/bug-in-bun/aitube_ffmpeg/transform/cropBase64Video.ts index e3bcdc74786617aa7e7a6e8d294a8232ac978da9..12544b932be3bdf674d32dd7ea41b48cad5dcaa6 100644 --- a/src/core/ffmpeg/cropBase64Video.mts +++ b/src/bug-in-bun/aitube_ffmpeg/transform/cropBase64Video.ts @@ -1,8 +1,8 @@ -import { promises as fs } from "node:fs"; -import os from "node:os"; -import path from "node:path"; +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" -import ffmpeg from "fluent-ffmpeg"; +import ffmpeg from "fluent-ffmpeg" export async function cropBase64Video({ base64Video, @@ -42,8 +42,8 @@ export async function cropBase64Video({ } const { width: inWidth, height: inHeight } = videoStream; - const x = Math.floor((inWidth - width) / 2); - const y = Math.floor((inHeight - height) / 2); + const x = Math.floor(((inWidth || 0) - width) / 2); + const y = Math.floor(((inHeight || 0) - height) / 2); ffmpeg(inputVideoPath) .outputOptions([ diff --git a/src/core/ffmpeg/cropVideo.mts b/src/bug-in-bun/aitube_ffmpeg/transform/cropVideo.ts similarity index 87% rename from src/core/ffmpeg/cropVideo.mts rename to src/bug-in-bun/aitube_ffmpeg/transform/cropVideo.ts index 68bb8026ebe44aaf9ada3e8b7132b8e14ac3fea4..74c1219d93aa24076ff743d19c994b3ad91e8de8 100644 --- a/src/core/ffmpeg/cropVideo.mts +++ b/src/bug-in-bun/aitube_ffmpeg/transform/cropVideo.ts @@ -1,9 +1,8 @@ -import { promises as fs } from "node:fs"; -// import { writeFile, readFile } from 'node:fs/promises'; -import os from "node:os"; -import path from "node:path"; +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" -import ffmpeg from "fluent-ffmpeg"; +import ffmpeg from "fluent-ffmpeg" export async function cropVideo({ inputVideoPath, @@ -42,9 +41,10 @@ export async function cropVideo({ return; } - const { width: inWidth, height: inHeight } = videoStream; - const x = Math.floor((inWidth - width) / 2); - const y = Math.floor((inHeight - height) / 2); + const { width: inWidth, height: inHeight } = videoStream + + const x = Math.floor(((inWidth || 0) - width) / 2) + const y = Math.floor(((inHeight || 0) - height) / 2) ffmpeg(inputVideoPath) .outputOptions([ diff --git a/src/bug-in-bun/aitube_ffmpeg/transform/index.ts b/src/bug-in-bun/aitube_ffmpeg/transform/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1a4ae7b7425ce47b605830e80da0f0f0e7c2f5a --- /dev/null +++ b/src/bug-in-bun/aitube_ffmpeg/transform/index.ts @@ -0,0 +1,3 @@ +export { cropBase64Video } from "./cropBase64Video" +export { cropVideo } from "./cropVideo" +export { scaleVideo } from "./scaleVideo" diff --git a/src/core/ffmpeg/scaleVideo.mts b/src/bug-in-bun/aitube_ffmpeg/transform/scaleVideo.ts similarity index 88% rename from src/core/ffmpeg/scaleVideo.mts rename to src/bug-in-bun/aitube_ffmpeg/transform/scaleVideo.ts index 70a4aa3eada2f917f8453588b00c56464ee43efc..ecd09ddabf5c8bfd034bb366a268d1efd2b8d358 100644 --- a/src/core/ffmpeg/scaleVideo.mts +++ b/src/bug-in-bun/aitube_ffmpeg/transform/scaleVideo.ts @@ -1,10 +1,9 @@ -import fs from 'node:fs/promises'; -import { writeFile, readFile } from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; +import { rm, mkdtemp, writeFile, readFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' -import { v4 as uuidv4 } from "uuid"; -import ffmpeg from 'fluent-ffmpeg'; +import { v4 as uuidv4 } from "uuid" +import ffmpeg from 'fluent-ffmpeg' export type ScaleVideoParams = { input: string; @@ -25,8 +24,6 @@ export type ScaleVideoParams = { * Upon completion, the temporary output file is read into a buffer, converted to a base64 string with the correct prefix, and then cleaned up by removing temporary files. * To call this function with desired input and height, you'd use it similarly to the provided convertMp4ToMp3 function example, being mindful that input must be a file path or properly-formatted base64 string and height is a number representing the new height of the video. * - * Enter your message... - * * @param param0 * @returns */ @@ -36,7 +33,7 @@ export async function scaleVideo({ asBase64 = false, debug = false }: ScaleVideoParams): Promise { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffmpeg-")); + const tempDir = await mkdtemp(path.join(os.tmpdir(), "ffmpeg-")); const tempOutPath = path.join(tempDir, `${uuidv4()}.mp4`); let inputPath; @@ -82,7 +79,7 @@ export async function scaleVideo({ reject(new Error(`Error loading the video file: ${error}`)); } finally { // Clean up temporary files - await fs.rm(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true }); } }) .save(tempOutPath); diff --git a/src/core/base64/addBase64.mts b/src/core/base64/addBase64.mts deleted file mode 100644 index 5c724a3d069d7d2f02a1907372df47e02f0c63fc..0000000000000000000000000000000000000000 --- a/src/core/base64/addBase64.mts +++ /dev/null @@ -1,51 +0,0 @@ -export function addBase64Header( - image?: string, - format?: - | "jpeg" | "jpg" | "png" | "webp" | "heic" - | "mp3" | "wav" - | "mp4" | "webm" - | string -) { - - if (!image || typeof image !== "string" || image.length < 60) { - return "" - } - - const ext = (`${format || ""}`.split(".").pop() || "").toLowerCase().trim() - - let mime = "" - if ( - ext === "jpeg" || - ext === "jpg") { - mime = "image/jpeg" - } else if ( - ext === "webp" - ) { - mime = "image/webp" - } else if ( - ext === "png") { - mime = "image/png" - } else if (ext === "heic") { - mime = "image/heic" - } else if (ext === "mp3") { - mime = "audio/mp3" - } else if (ext === "mp4") { - mime = "video/mp4" - } else if (ext === "webm") { - mime = "video/webm" - } else if (ext === "wav") { - mime = "audio/wav" - } else { - throw new Error(`addBase64Header failed (unsupported format: ${format})`) - } - - if (image.startsWith('data:')) { - if (image.startsWith(`data:${mime};base64,`)) { - return image - } else { - throw new Error(`addBase64Header failed (input string is NOT a ${mime} image)`) - } - } else { - return `data:${mime};base64,${image}` - } -} \ No newline at end of file diff --git a/src/core/base64/dataUriToBlob.mts b/src/core/base64/dataUriToBlob.mts deleted file mode 100644 index f60f4250d3ed35f9968a2f1fb18fa0d50940915f..0000000000000000000000000000000000000000 --- a/src/core/base64/dataUriToBlob.mts +++ /dev/null @@ -1,15 +0,0 @@ - -export function dataUriToBlob(dataURI = "", defaultContentType = ""): Blob { - dataURI = dataURI.replace(/^data:/, ''); - - const type = dataURI.match(/(?:image|application|video|audio|text)\/[^;]+/)?.[0] || defaultContentType; - const base64 = dataURI.replace(/^[^,]+,/, ''); - const arrayBuffer = new ArrayBuffer(base64.length); - const typedArray = new Uint8Array(arrayBuffer); - - for (let i = 0; i < base64.length; i++) { - typedArray[i] = base64.charCodeAt(i); - } - - return new Blob([arrayBuffer], { type }); -} \ No newline at end of file diff --git a/src/core/base64/extractBase64.mts b/src/core/base64/extractBase64.mts deleted file mode 100644 index 5527fa9f9b341375d616cecd768b602c37e5a7b7..0000000000000000000000000000000000000000 --- a/src/core/base64/extractBase64.mts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * break a base64 string into sub-components - */ -export function extractBase64(base64: string = ""): { - - // file format eg. video/mp4 text/html audio/wave - mimetype: string; - - // file extension eg. .mp4 .html .wav - extension: string; - - data: string; - buffer: Buffer; - blob: Blob; -} { - // console.log(`extractBase64(${base64.slice(0, 120)})`) - // Regular expression to extract the MIME type and the base64 data - const matches = base64.match(/^data:([A-Za-z-+0-9/]+);base64,(.+)$/) - - if (!matches || matches.length !== 3) { - throw new Error("Invalid base64 string") - } - - const mimetype = matches[1] || "" - const data = matches[2] || "" - const buffer = Buffer.from(data, "base64") - const blob = new Blob([buffer]) - - // this should be enough for most media formats (jpeg, png, webp, mp4) - const extension = mimetype.split("/").pop() || "" - - return { - mimetype, - extension, - data, - buffer, - blob, - } -} \ No newline at end of file diff --git a/src/core/converters/blobToWebp.mts b/src/core/converters/blobToWebp.mts deleted file mode 100644 index 4a194a50a65b71ed9c056b31e6a81af6b27ba245..0000000000000000000000000000000000000000 --- a/src/core/converters/blobToWebp.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function blobToWebp(blob: Blob) { - return addBase64Header(Buffer.from(await blob.text()).toString('base64'), "webp") -} \ No newline at end of file diff --git a/src/core/converters/bufferToJpeg.mts b/src/core/converters/bufferToJpeg.mts deleted file mode 100644 index 4b7f6835b4c91992dbb8d1f0ac8b2e05df088746..0000000000000000000000000000000000000000 --- a/src/core/converters/bufferToJpeg.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function bufferToJpeg(buffer: Buffer) { - return addBase64Header(buffer.toString('base64'), "jpeg") -} \ No newline at end of file diff --git a/src/core/converters/bufferToMp3.mts b/src/core/converters/bufferToMp3.mts deleted file mode 100644 index 0ea8cbb26d35cc50dfbdca18d637c3d49f669b93..0000000000000000000000000000000000000000 --- a/src/core/converters/bufferToMp3.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function bufferToMp3(buffer: Buffer) { - return addBase64Header(buffer.toString('base64'), "mp3") -} \ No newline at end of file diff --git a/src/core/converters/bufferToMp4.mts b/src/core/converters/bufferToMp4.mts deleted file mode 100644 index 64e9a102bba579451ad0ce595fced48ee633c1b9..0000000000000000000000000000000000000000 --- a/src/core/converters/bufferToMp4.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function bufferToMp4(buffer: Buffer) { - return addBase64Header(buffer.toString('base64'), "mp4") -} \ No newline at end of file diff --git a/src/core/converters/bufferToPng.mts b/src/core/converters/bufferToPng.mts deleted file mode 100644 index 0c18a39d9182b5d2c9fa0b9bbda7e282ec100ac5..0000000000000000000000000000000000000000 --- a/src/core/converters/bufferToPng.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function bufferToPng(buffer: Buffer) { - return addBase64Header(buffer.toString('base64'), "png") -} \ No newline at end of file diff --git a/src/core/converters/bufferToWav.mts b/src/core/converters/bufferToWav.mts deleted file mode 100644 index bf083aac9a7c8dcf2184e8b2f523048006fb4b77..0000000000000000000000000000000000000000 --- a/src/core/converters/bufferToWav.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function bufferToWav(buffer: Buffer) { - return addBase64Header(buffer.toString('base64'), "wav") -} \ No newline at end of file diff --git a/src/core/converters/bufferToWebp.mts b/src/core/converters/bufferToWebp.mts deleted file mode 100644 index 430730b2ffc439363aac29fb198df9815deb4c9b..0000000000000000000000000000000000000000 --- a/src/core/converters/bufferToWebp.mts +++ /dev/null @@ -1,5 +0,0 @@ -import { addBase64Header } from "../base64/addBase64.mts"; - -export async function bufferToWebp(buffer: Buffer) { - return addBase64Header(buffer.toString('base64'), "webp") -} \ No newline at end of file diff --git a/src/core/converters/convertImageTo.mts b/src/core/converters/convertImageTo.mts deleted file mode 100644 index 5909c27a111561fef6c0ea7a8d029b194d90f065..0000000000000000000000000000000000000000 --- a/src/core/converters/convertImageTo.mts +++ /dev/null @@ -1,31 +0,0 @@ -import { convertImageToJpeg } from "./convertImageToJpeg.mts" -import { convertImageToPng } from "./convertImageToPng.mts" -import { convertImageToWebp } from "./convertImageToWebp.mts" -import { ImageFileExt } from "./imageFormats.mts" - -/** - * Convert an image to one of the supported file formats - * - * @param imgBase64 - * @param outputFormat - * @returns - */ -export async function convertImageTo(imgBase64: string = "", outputFormat: ImageFileExt): Promise { - const format = outputFormat.trim().toLowerCase() as ImageFileExt - if (!["jpeg", "jpg", "png", "webp"].includes(format)) { - throw new Error(`unsupported file format "${format}"`) - } - - const isJpeg = format === "jpg" || format === "jpeg" - - - if (isJpeg) { - return convertImageToJpeg(imgBase64) - } - - if (format === "webp") { - return convertImageToWebp(imgBase64) - } - - return convertImageToPng(imgBase64) -} diff --git a/src/core/converters/convertImageToJpeg.mts b/src/core/converters/convertImageToJpeg.mts deleted file mode 100644 index 7ef63e91a8b83873d9a088c5483d0c1bc4d3d454..0000000000000000000000000000000000000000 --- a/src/core/converters/convertImageToJpeg.mts +++ /dev/null @@ -1,27 +0,0 @@ -import sharp from "sharp" - -export async function convertImageToJpeg(imgBase64: string = "", quality: number = 92): Promise { - - const base64WithoutHeader = imgBase64.split(";base64,")[1] || "" - - if (!base64WithoutHeader) { - const slice = `${imgBase64 || ""}`.slice(0, 50) - throw new Error(`couldn't process input image "${slice}..."`) - } - - // Convert base64 to buffer - const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64') - - // Resize the buffer to the target size - const newBuffer = await sharp(tmpBuffer) - .jpeg({ - quality, - // we don't use progressive: true because we pre-load images anyway - }) - .toBuffer() - - // Convert the buffer back to base64 - const newImageBase64 = newBuffer.toString('base64') - - return `data:image/jpeg;base64,${newImageBase64}` -} \ No newline at end of file diff --git a/src/core/converters/convertImageToOriginal.mts b/src/core/converters/convertImageToOriginal.mts deleted file mode 100644 index 92971f64b13d98f8700c5b76190bfa6925c47beb..0000000000000000000000000000000000000000 --- a/src/core/converters/convertImageToOriginal.mts +++ /dev/null @@ -1,6 +0,0 @@ - -// you are reading it right: this function does.. nothing! -// it is a NOOP conversion function -export async function convertImageToOriginal(imgBase64: string = ""): Promise { - return imgBase64 -} \ No newline at end of file diff --git a/src/core/converters/convertImageToPng.mts b/src/core/converters/convertImageToPng.mts deleted file mode 100644 index 8edd35bc9b728d814134dea7a5b70bd06b8f83a9..0000000000000000000000000000000000000000 --- a/src/core/converters/convertImageToPng.mts +++ /dev/null @@ -1,23 +0,0 @@ -import sharp from "sharp" - -export async function convertImageToPng(imgBase64: string = ""): Promise { - - const base64WithoutHeader = imgBase64.split(";base64,")[1] || "" - - if (!base64WithoutHeader) { - const slice = `${imgBase64 || ""}`.slice(0, 50) - throw new Error(`couldn't process input image "${slice}..."`) - } - - // Convert base64 to buffer - const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64') - - const newBuffer = await sharp(tmpBuffer) - .png() - .toBuffer() - - // Convert the buffer back to base64 - const newImageBase64 = newBuffer.toString('base64') - - return `data:image/png;base64,${newImageBase64}` -} \ No newline at end of file diff --git a/src/core/converters/convertImageToWebp.mts b/src/core/converters/convertImageToWebp.mts deleted file mode 100644 index 2d936faa021aca1b7bafc3847833d69041fabe9d..0000000000000000000000000000000000000000 --- a/src/core/converters/convertImageToWebp.mts +++ /dev/null @@ -1,41 +0,0 @@ -import sharp from "sharp" - -export async function convertImageToWebp(imgBase64: string = ""): Promise { - - const base64WithoutHeader = imgBase64.split(";base64,")[1] || "" - - if (!base64WithoutHeader) { - const slice = `${imgBase64 || ""}`.slice(0, 50) - throw new Error(`couldn't process input image "${slice}..."`) - } - - // Convert base64 to buffer - const tmpBuffer = Buffer.from(base64WithoutHeader, 'base64') - - // Resize the buffer to the target size - const newBuffer = await sharp(tmpBuffer) - .webp({ - // for options please see https://sharp.pixelplumbing.com/api-output#webp - - // preset: "photo", - - // effort: 3, - - // for a PNG-like quality - // lossless: true, - - // by default it is quality 80 - quality: 80, - - // nearLossless: true, - - // use high quality chroma subsampling - smartSubsample: true, - }) - .toBuffer() - - // Convert the buffer back to base64 - const newImageBase64 = newBuffer.toString('base64') - - return `data:image/webp;base64,${newImageBase64}` -} \ No newline at end of file diff --git a/src/core/converters/imageFormats.mts b/src/core/converters/imageFormats.mts deleted file mode 100644 index 027dfd62d46e180fc8d06fbaf5ac232af90e3008..0000000000000000000000000000000000000000 --- a/src/core/converters/imageFormats.mts +++ /dev/null @@ -1 +0,0 @@ -export type ImageFileExt = "png" | "jpeg" | "jpg" | "webp" diff --git a/src/core/exporters/clapWithStoryboardsToVideoFile.mts b/src/core/exporters/clapWithStoryboardsToVideoFile.ts similarity index 89% rename from src/core/exporters/clapWithStoryboardsToVideoFile.mts rename to src/core/exporters/clapWithStoryboardsToVideoFile.ts index 06cd919460076325e0b67fc9cf49bef26eac32ca..db0450eb8e9b2a0c9edc455539a0905b2274d612 100644 --- a/src/core/exporters/clapWithStoryboardsToVideoFile.mts +++ b/src/core/exporters/clapWithStoryboardsToVideoFile.ts @@ -1,7 +1,7 @@ import { ClapProject, ClapSegment } from "@aitube/clap" +import { getRandomDirectory } from "@aitube/io" -import { getRandomDirectory } from "../files/getRandomDirectory.mts" -import { storyboardSegmentToVideoFile } from "./storyboardSegmentToVideoFile.mts" +import { storyboardSegmentToVideoFile } from "./storyboardSegmentToVideoFile" export async function clapWithStoryboardsToVideoFile({ clap, diff --git a/src/core/exporters/clapWithVideosToVideoFile.mts b/src/core/exporters/clapWithVideosToVideoFile.ts similarity index 90% rename from src/core/exporters/clapWithVideosToVideoFile.mts rename to src/core/exporters/clapWithVideosToVideoFile.ts index e813b0db052230220ae5e5b413f3f72036ade183..7311ce6033d44d6aa42edb55dc74b5843f53d070 100644 --- a/src/core/exporters/clapWithVideosToVideoFile.mts +++ b/src/core/exporters/clapWithVideosToVideoFile.ts @@ -1,8 +1,7 @@ import { ClapProject, ClapSegment } from "@aitube/clap" +import { getRandomDirectory } from "@aitube/io" -import { getRandomDirectory } from "../files/getRandomDirectory.mts" -import { videoSegmentToVideoFile } from "./videoSegmentToVideoFile.mts" - +import { videoSegmentToVideoFile } from "./videoSegmentToVideoFile" export async function clapWithVideosToVideoFile({ clap, diff --git a/src/core/exporters/storyboardSegmentToVideoFile.mts b/src/core/exporters/storyboardSegmentToVideoFile.ts similarity index 89% rename from src/core/exporters/storyboardSegmentToVideoFile.mts rename to src/core/exporters/storyboardSegmentToVideoFile.ts index fe729d419e98f70e73f37499b15fddfbe158c583..87e7e0e3889244f9a975e44e62d672e8b0fe804d 100644 --- a/src/core/exporters/storyboardSegmentToVideoFile.mts +++ b/src/core/exporters/storyboardSegmentToVideoFile.ts @@ -1,14 +1,12 @@ import { join } from "node:path" import { ClapProject, ClapSegment } from "@aitube/clap" +import { extractBase64 } from "@aitube/encoders" +import { deleteFile, writeBase64ToFile } from "@aitube/io" +//import { addTextToVideo, concatenateVideosWithAudio, imageToVideoBase64 } from "@aitube/ffmpeg" +import { addTextToVideo, concatenateVideosWithAudio, imageToVideoBase64 } from "../../bug-in-bun/aitube_ffmpeg" -import { concatenateVideosWithAudio } from "../ffmpeg/concatenateVideosWithAudio.mts" -import { writeBase64ToFile } from "../files/writeBase64ToFile.mts" -import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts" -import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts" -import { deleteFile } from "../files/deleteFile.mts" -import { extractBase64 } from "../base64/extractBase64.mts" -import { imageToVideoBase64 } from "../ffmpeg/imageToVideoBase64.mts" +import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2" export async function storyboardSegmentToVideoFile({ clap, diff --git a/src/core/exporters/videoSegmentToVideoFile.mts b/src/core/exporters/videoSegmentToVideoFile.ts similarity index 88% rename from src/core/exporters/videoSegmentToVideoFile.mts rename to src/core/exporters/videoSegmentToVideoFile.ts index c7d6ee9974d665b038c4968492f895f0851db78d..6f1aac94c93ad3308db7f355779950df51805768 100644 --- a/src/core/exporters/videoSegmentToVideoFile.mts +++ b/src/core/exporters/videoSegmentToVideoFile.ts @@ -1,14 +1,12 @@ import { join } from "node:path" import { ClapProject, ClapSegment } from "@aitube/clap" +import { extractBase64 } from "@aitube/encoders" +import { deleteFile, writeBase64ToFile } from "@aitube/io" +// import { addTextToVideo, concatenateVideosWithAudio } from "@aitube/ffmpeg" +import { addTextToVideo, concatenateVideosWithAudio } from "../../bug-in-bun/aitube_ffmpeg" -import { concatenateVideosWithAudio } from "../ffmpeg/concatenateVideosWithAudio.mts" -import { writeBase64ToFile } from "../files/writeBase64ToFile.mts" -import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts" -import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts" -import { deleteFile } from "../files/deleteFile.mts" -import { extractBase64 } from "../base64/extractBase64.mts" - +import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2" export async function videoSegmentToVideoFile({ clap, diff --git a/src/core/files/deleteFile.mts b/src/core/files/deleteFile.mts deleted file mode 100644 index 29e3d5b14b87ef99cdaceb43c43d8cf4e0aa7167..0000000000000000000000000000000000000000 --- a/src/core/files/deleteFile.mts +++ /dev/null @@ -1,14 +0,0 @@ -import { unlink, rm } from "node:fs/promises" - -export async function deleteFile(filePath: string, debug?: boolean): Promise { - try { - await rm(filePath, { recursive: true, force: true }) - // await unlink(filePath) - return true - } catch (err) { - if (debug) { - console.error(`failed to unlink file at ${filePath}: ${err}`) - } - } - return false -} \ No newline at end of file diff --git a/src/core/files/deleteFileWithName.mts b/src/core/files/deleteFileWithName.mts deleted file mode 100644 index 36e61de4849418813617676867d730beb286a45a..0000000000000000000000000000000000000000 --- a/src/core/files/deleteFileWithName.mts +++ /dev/null @@ -1,12 +0,0 @@ -import { promises as fs } from "node:fs" -import path from "node:path" -import { deleteFile } from "./deleteFile.mts" - -export const deleteFilesWithName = async (dir: string, name: string, debug?: boolean) => { - console.log(`deleteFilesWithName(${dir}, ${name})`) - for (const file of await fs.readdir(dir)) { - if (file.includes(name)) { - await deleteFile(path.join(dir, file)) - } - } -} diff --git a/src/core/files/downloadFileAsBase64.mts b/src/core/files/downloadFileAsBase64.mts deleted file mode 100644 index 526fb07e29f0982385a5fd4306b7df02b1f25667..0000000000000000000000000000000000000000 --- a/src/core/files/downloadFileAsBase64.mts +++ /dev/null @@ -1,27 +0,0 @@ -import { lookup } from "mime-types" - -export const downloadFileAsBase64 = async (remoteUrl: string): Promise => { - // const controller = new AbortController() - - // download the file - const response = await fetch(remoteUrl, { - // signal: controller.signal - }) - - // get as Buffer - const arrayBuffer = await response.arrayBuffer() - const buffer = Buffer.from(arrayBuffer) - - // convert it to base64 - const base64 = buffer.toString('base64') - - - const res = lookup(remoteUrl) - let contentType = res.toString() - if (typeof res === "boolean" && res === false) { - contentType = response.headers.get('content-type') - } - - const assetUrl = `data:${contentType};base64,${base64}` - return assetUrl -}; \ No newline at end of file diff --git a/src/core/files/getRandomDirectory.mts b/src/core/files/getRandomDirectory.mts deleted file mode 100644 index a18644699fc90ceadcfd682c40e5dd3723a22102..0000000000000000000000000000000000000000 --- a/src/core/files/getRandomDirectory.mts +++ /dev/null @@ -1,8 +0,0 @@ -import { tmpdir } from "node:os" -import { join } from "node:path" -import { mkdtemp } from "node:fs/promises" -import { v4 as uuidv4 } from "uuid" - -export async function getRandomDirectory(): Promise { - return mkdtemp(join(tmpdir(), uuidv4())) -} \ No newline at end of file diff --git a/src/core/files/readJpegFileToBase64.mts b/src/core/files/readJpegFileToBase64.mts deleted file mode 100644 index 26be11ffe9831c3d857f509515f42b410c8f43ef..0000000000000000000000000000000000000000 --- a/src/core/files/readJpegFileToBase64.mts +++ /dev/null @@ -1,18 +0,0 @@ -import { promises as fs } from "fs" - -export async function readJpegFileToBase64(filePath: string): Promise { - try { - // Read the file's content as a Buffer - const fileBuffer = await fs.readFile(filePath); - - // Convert the buffer to a base64 string - const base64 = fileBuffer.toString('base64'); - - // Prefix the base64 string with the Data URI scheme for PNG images - return `data:image/jpeg;base64,${base64}`; - } catch (error) { - // Handle errors (e.g., file not found, no permissions, etc.) - console.error(error); - throw error; - } -} diff --git a/src/core/files/readMp3FileToBase64.mts b/src/core/files/readMp3FileToBase64.mts deleted file mode 100644 index 994f47dbf77f3b5d4a307fb97c6242b1b6668530..0000000000000000000000000000000000000000 --- a/src/core/files/readMp3FileToBase64.mts +++ /dev/null @@ -1,18 +0,0 @@ -import { promises as fs } from "fs" - -export async function readMp3FileToBase64(filePath: string): Promise { - try { - // Read the file's content as a Buffer - const fileBuffer = await fs.readFile(filePath); - - // Convert the buffer to a base64 string - const base64 = fileBuffer.toString('base64'); - - // Prefix the base64 string with the Data URI scheme for PNG images - return `data:audio/mp3;base64,${base64}`; - } catch (error) { - // Handle errors (e.g., file not found, no permissions, etc.) - console.error(error); - throw error; - } -} diff --git a/src/core/files/readMp4FileToBase64.mts b/src/core/files/readMp4FileToBase64.mts deleted file mode 100644 index b6b1f4c20d3584ed1f34beb6daa12314237120a5..0000000000000000000000000000000000000000 --- a/src/core/files/readMp4FileToBase64.mts +++ /dev/null @@ -1,18 +0,0 @@ -import { promises as fs } from "fs" - -export async function readMp4FileToBase64(filePath: string): Promise { - try { - // Read the file's content as a Buffer - const fileBuffer = await fs.readFile(filePath); - - // Convert the buffer to a base64 string - const base64 = fileBuffer.toString('base64'); - - // Prefix the base64 string with the Data URI scheme for PNG images - return `data:video/mp4;base64,${base64}`; - } catch (error) { - // Handle errors (e.g., file not found, no permissions, etc.) - console.error(error); - throw error; - } -} diff --git a/src/core/files/readPlainText.mts b/src/core/files/readPlainText.mts deleted file mode 100644 index 81642cd2ac183432b76bc3a36a214033c1fb5731..0000000000000000000000000000000000000000 --- a/src/core/files/readPlainText.mts +++ /dev/null @@ -1,13 +0,0 @@ -import { promises as fs } from "fs" - -export async function readPlainText(filePath: string): Promise { - try { - const plainText = await fs.readFile(filePath, "utf-8"); - - return plainText; - } catch (error) { - // Handle errors (e.g., file not found, no permissions, etc.) - console.error(error); - throw error; - } -} diff --git a/src/core/files/readPngFileToBase64.mts b/src/core/files/readPngFileToBase64.mts deleted file mode 100644 index 8ce9649ccf55bf717cd86e194c2f23c4daded351..0000000000000000000000000000000000000000 --- a/src/core/files/readPngFileToBase64.mts +++ /dev/null @@ -1,18 +0,0 @@ -import { promises as fs } from "fs" - -export async function readPngFileToBase64(filePath: string): Promise { - try { - // Read the file's content as a Buffer - const fileBuffer = await fs.readFile(filePath); - - // Convert the buffer to a base64 string - const base64 = fileBuffer.toString('base64'); - - // Prefix the base64 string with the Data URI scheme for PNG images - return `data:image/png;base64,${base64}`; - } catch (error) { - // Handle errors (e.g., file not found, no permissions, etc.) - console.error(error); - throw error; - } -} diff --git a/src/core/files/readWavFileToBase64.mts b/src/core/files/readWavFileToBase64.mts deleted file mode 100644 index c9ba1281b6817e85f88da5efe2f424a2d11c4eda..0000000000000000000000000000000000000000 --- a/src/core/files/readWavFileToBase64.mts +++ /dev/null @@ -1,18 +0,0 @@ -import { promises as fs } from "fs" - -export async function readWavFileToBase64(filePath: string): Promise { - try { - // Read the file's content as a Buffer - const fileBuffer = await fs.readFile(filePath); - - // Convert the buffer to a base64 string - const base64 = fileBuffer.toString('base64'); - - // Prefix the base64 string with the Data URI scheme for PNG images - return `data:audio/wav;base64,${base64}`; - } catch (error) { - // Handle errors (e.g., file not found, no permissions, etc.) - console.error(error); - throw error; - } -} diff --git a/src/core/files/removeTmpFiles.mts b/src/core/files/removeTmpFiles.mts deleted file mode 100644 index d290be827f07fd9a6d1b91c558baf2fdfc82f202..0000000000000000000000000000000000000000 --- a/src/core/files/removeTmpFiles.mts +++ /dev/null @@ -1,21 +0,0 @@ -import { existsSync, promises as fs } from "node:fs" - -// note: this function will never fail -export async function removeTemporaryFiles(filesPaths: string[]) { - try { - // Cleanup temporary files - you could choose to do this or leave it to the user - await Promise.all(filesPaths.map(async (filePath) => { - try { - if (existsSync(filePath)) { - await fs.rm(filePath) - } - } catch (err) { - // - } - })) - } catch (err) { - // no big deal, except a bit of tmp file leak - // although.. if delete failed, it could also indicate - // that the file has already been cleaned-up, so even better! - } -} \ No newline at end of file diff --git a/src/core/files/resizeImage.mts b/src/core/files/resizeImage.mts deleted file mode 100644 index 5a1b53f2a79abd0f523dbe7fd69c1fc1fbccfb62..0000000000000000000000000000000000000000 --- a/src/core/files/resizeImage.mts +++ /dev/null @@ -1,55 +0,0 @@ -import sharp from "sharp"; - -export type ResizeImageParams = { - input: string - width?: number - height?: number - debug?: boolean - asBase64?: boolean // TODO: not implemented yet! -}; - -/** - * Resize an image to a given width and height. - * The input image can be a file path or a data URI (base64) - * The image ratio will be preserved if only one side is given. - * The image format (WebP, Jpeg, PNG) will be preserved. - * This function always return a base64 string (data URI with the mime type) - * - * @param param0 - * @returns - */ -export async function resizeImage({ input, width, height, debug, asBase64 }: ResizeImageParams): Promise { - let inputBuffer: Buffer; - - // Test if input is a data URI - const dataUriPattern = /^data:([a-zA-Z]+\/[a-zA-Z]+);base64,(.*)$/; - const matches = input.match(dataUriPattern); - - if (matches) { - const [, mimeType, base64Data] = matches; - if (!/^image\/(png|jpeg|webp)$/.test(mimeType)) { - throw new Error(`Unsupported image format. Expected PNG, JPEG, or WebP.`); - } - inputBuffer = Buffer.from(base64Data, "base64"); - } else { - // Assuming input is a file path - inputBuffer = await sharp(input).toBuffer(); - } - - const sharpInstance = sharp(inputBuffer) - .resize(width, height, { - fit: "inside", - withoutEnlargement: true - }); - - const outputBuffer = await sharpInstance.toBuffer(); - const outputMimeType = await sharpInstance.metadata().then(meta => meta.format); - - if (!outputMimeType) { - throw new Error("Failed to determine the image mime type after resizing."); - } - - const prefix = `data:image/${outputMimeType};base64,`; - const outputBase64 = outputBuffer.toString("base64"); - return `${prefix}${outputBase64}`; -} \ No newline at end of file diff --git a/src/core/files/writeBase64ToFile.mts b/src/core/files/writeBase64ToFile.mts deleted file mode 100644 index 27149a54662a2044dfa49ba906786b857b9f6dc0..0000000000000000000000000000000000000000 --- a/src/core/files/writeBase64ToFile.mts +++ /dev/null @@ -1,29 +0,0 @@ -import { promises as fs } from "node:fs" - -export async function writeBase64ToFile(base64Data: string, filePath: string): Promise { - const data = base64Data.split(";base64,").pop() - if (!data) { throw new Error("Invalid base64 content") } - await fs.writeFile(filePath, data, { encoding: "base64" }) - return filePath -} - -// legacy way: with more manual steps - -/* -export async function writeBase64ToFile(content: string, filePath: string): Promise { - - // Remove "data:image/png;base64," from the start of the data url - const base64Data = content.split(";base64,")[1] - - // Convert base64 to binary - const data = Buffer.from(base64Data, "base64") - - // Write binary data to file - try { - await fs.writeFile(filePath, data) - // console.log("File written successfully") - } catch (error) { - console.error("An error occurred:", error) - } -} -*/ \ No newline at end of file diff --git a/src/core/utils/formatProgress.mts b/src/core/utils/formatProgress.ts similarity index 100% rename from src/core/utils/formatProgress.mts rename to src/core/utils/formatProgress.ts diff --git a/src/core/parsers/getValidNumber.mts b/src/core/utils/getValidNumber.ts similarity index 100% rename from src/core/parsers/getValidNumber.mts rename to src/core/utils/getValidNumber.ts diff --git a/src/core/utils/isValidNumber.mts b/src/core/utils/isValidNumber.ts similarity index 100% rename from src/core/utils/isValidNumber.mts rename to src/core/utils/isValidNumber.ts diff --git a/src/core/parsers/parseArray.mts b/src/core/utils/parseArray.ts similarity index 100% rename from src/core/parsers/parseArray.mts rename to src/core/utils/parseArray.ts diff --git a/src/core/utils/sleep.mts b/src/core/utils/sleep.ts similarity index 100% rename from src/core/utils/sleep.mts rename to src/core/utils/sleep.ts diff --git a/src/core/utils/startOfSegment1IsWithinSegment2.mts b/src/core/utils/startOfSegment1IsWithinSegment2.ts similarity index 81% rename from src/core/utils/startOfSegment1IsWithinSegment2.mts rename to src/core/utils/startOfSegment1IsWithinSegment2.ts index 980d15d6f1090a771aec0e80d48bcebfc778a4ac..0950d64c2d1801e297404dcbad9ab5b940263919 100644 --- a/src/core/utils/startOfSegment1IsWithinSegment2.mts +++ b/src/core/utils/startOfSegment1IsWithinSegment2.ts @@ -1,4 +1,4 @@ -import { ClapSegment } from "../clap/types.mts"; +import { ClapSegment } from "@aitube/clap" export function startOfSegment1IsWithinSegment2(s1: ClapSegment, s2: ClapSegment) { const startOfSegment1 = s1.startTimeInMs diff --git a/src/core/utils/timeout.mts b/src/core/utils/timeout.ts similarity index 100% rename from src/core/utils/timeout.mts rename to src/core/utils/timeout.ts diff --git a/src/index.mts b/src/index.ts similarity index 90% rename from src/index.mts rename to src/index.ts index 654cf6e00a89e39849b184a0e9a03b2a572545f0..f561fd4c30992d00565b009973a2d2ff1b9558ae 100644 --- a/src/index.mts +++ b/src/index.ts @@ -5,9 +5,10 @@ import express from "express" import queryString from "query-string" import { parseClap, ClapProject } from "@aitube/clap" -import { clapToTmpVideoFilePath } from "./main.mts" -import { deleteFile } from "./core/files/deleteFile.mts" -import { defaultExportFormat, SupportedExportFormat } from "./core/ffmpeg/concatenateVideosWithAudio.mts" +import { clapToTmpVideoFilePath } from "./main" +// import { defaultExportFormat, type SupportedExportFormat } from "@aitube/ffmpeg" +import { defaultExportFormat, type SupportedExportFormat } from "./bug-in-bun/aitube_ffmpeg" +import { deleteFile } from "@aitube/io" const app = express() const port = 7860 diff --git a/src/main.mts b/src/main.ts similarity index 89% rename from src/main.mts rename to src/main.ts index 268afd76cd23884f4583feeddcd8339e6e8075aa..0e6f5299beafc98fa0c16d9b995ab8d6aedbbf0c 100644 --- a/src/main.mts +++ b/src/main.ts @@ -1,16 +1,19 @@ import { join } from "node:path" -import { ClapProject } from "@aitube/clap"; - -import { concatenateAudio, ConcatenateAudioOutput } from "./core/ffmpeg/concatenateAudio.mts"; -import { concatenateVideosWithAudio, defaultExportFormat, SupportedExportFormat } from "./core/ffmpeg/concatenateVideosWithAudio.mts"; -import { writeBase64ToFile } from "./core/files/writeBase64ToFile.mts"; -import { concatenateVideos } from "./core/ffmpeg/concatenateVideos.mts" -import { deleteFilesWithName } from "./core/files/deleteFileWithName.mts" -import { getRandomDirectory } from "./core/files/getRandomDirectory.mts"; -import { clapWithVideosToVideoFile } from "./core/exporters/clapWithVideosToVideoFile.mts"; -import { clapWithStoryboardsToVideoFile } from "./core/exporters/clapWithStoryboardsToVideoFile.mts"; - +import { ClapProject } from "@aitube/clap" +import { deleteFilesWithName, getRandomDirectory, writeBase64ToFile } from "@aitube/io" +import { + concatenateAudio, + concatenateVideos, + concatenateVideosWithAudio, + defaultExportFormat, + type SupportedExportFormat, + type ConcatenateAudioOutput +// } from "@aitube/ffmpeg" +} from "./bug-in-bun/aitube_ffmpeg" + +import { clapWithStoryboardsToVideoFile } from "./core/exporters/clapWithStoryboardsToVideoFile" +import { clapWithVideosToVideoFile } from "./core/exporters/clapWithVideosToVideoFile" /** * Generate a .mp4 video inside a directory (if none is provided, it will be created in /tmp) diff --git a/tsconfig.json b/tsconfig.json index e314987ca04cba231fe991082e9a61c2068c0f27..69abaa510d05e9a389a8edc09d80f135daa5294f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "allowImportingTsExtensions": true, "target": "es2022" }, - "include": ["**/*.ts", "**/*.mts"], + "include": ["**/*.ts"], } \ No newline at end of file