Julian Bilcke
commited on
Commit
·
46fcec6
1
Parent(s):
1fc1c4d
another step for the Stories Factory (mp4 generation)
Browse files- src/core/base64/extractBase64.mts +5 -0
- src/core/converters/htmlToBase64Png.mts +1 -1
- src/core/ffmpeg/addTextToVideo.mts +28 -14
- src/core/files/deleteFile.mts +14 -0
- src/core/files/deleteFileWithName.mts +2 -8
- src/core/files/getRandomDirectory.mts +8 -0
- src/core/files/removeTmpFiles.mts +9 -13
- src/core/utils/startOfSegment1IsWithinSegment2.mts +6 -0
- src/index.mts +19 -8
- src/main.mts +108 -23
src/core/base64/extractBase64.mts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
| 2 |
* break a base64 string into sub-components
|
| 3 |
*/
|
| 4 |
export function extractBase64(base64: string = ""): {
|
|
|
|
|
|
|
| 5 |
mimetype: string;
|
|
|
|
|
|
|
| 6 |
extension: string;
|
|
|
|
| 7 |
data: string;
|
| 8 |
buffer: Buffer;
|
| 9 |
blob: Blob;
|
|
|
|
| 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;
|
src/core/converters/htmlToBase64Png.mts
CHANGED
|
@@ -28,7 +28,7 @@ export async function htmlToBase64Png({
|
|
| 28 |
}
|
| 29 |
|
| 30 |
const browser = await puppeteer.launch({
|
| 31 |
-
headless:
|
| 32 |
|
| 33 |
// apparently we need those, see:
|
| 34 |
// https://unix.stackexchange.com/questions/694734/puppeteer-in-alpine-docker-with-chromium-headless-dosent-seems-to-work
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
const browser = await puppeteer.launch({
|
| 31 |
+
headless: true,
|
| 32 |
|
| 33 |
// apparently we need those, see:
|
| 34 |
// https://unix.stackexchange.com/questions/694734/puppeteer-in-alpine-docker-with-chromium-headless-dosent-seems-to-work
|
src/core/ffmpeg/addTextToVideo.mts
CHANGED
|
@@ -1,23 +1,37 @@
|
|
| 1 |
-
import { createTextOverlayImage } from "./createTextOverlayImage.mts"
|
| 2 |
-
import { addImageToVideo } from "./addImageToVideo.mts"
|
|
|
|
| 3 |
|
| 4 |
-
export async function addTextToVideo(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
const
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
width: 1024 ,
|
| 11 |
-
height: 576,
|
| 12 |
})
|
| 13 |
-
console.log("filePath:", filePath)
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
const pathToVideo = await addImageToVideo({
|
| 17 |
inputVideoPath,
|
| 18 |
-
inputImagePath:
|
|
|
|
| 19 |
})
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
}
|
|
|
|
| 1 |
+
import { createTextOverlayImage } from "./createTextOverlayImage.mts"
|
| 2 |
+
import { addImageToVideo } from "./addImageToVideo.mts"
|
| 3 |
+
import { deleteFile } from "../files/deleteFile.mts"
|
| 4 |
|
| 5 |
+
export async function addTextToVideo({
|
| 6 |
+
inputVideoPath,
|
| 7 |
+
outputVideoPath,
|
| 8 |
+
text,
|
| 9 |
+
width,
|
| 10 |
+
height,
|
| 11 |
+
}: {
|
| 12 |
+
inputVideoPath: string
|
| 13 |
+
outputVideoPath: string
|
| 14 |
+
text: string
|
| 15 |
+
width: number
|
| 16 |
+
height: number
|
| 17 |
+
}): Promise<string> {
|
| 18 |
|
| 19 |
+
const { filePath: temporaryImageOverlayFilePath } = await createTextOverlayImage({
|
| 20 |
+
text,
|
| 21 |
+
width,
|
| 22 |
+
height,
|
|
|
|
|
|
|
| 23 |
})
|
|
|
|
| 24 |
|
| 25 |
+
console.log("addTextToVideo: temporaryImageOverlayFilePath:", temporaryImageOverlayFilePath)
|
| 26 |
+
|
| 27 |
const pathToVideo = await addImageToVideo({
|
| 28 |
inputVideoPath,
|
| 29 |
+
inputImagePath: temporaryImageOverlayFilePath,
|
| 30 |
+
outputVideoPath,
|
| 31 |
})
|
| 32 |
|
| 33 |
+
await deleteFile(temporaryImageOverlayFilePath)
|
| 34 |
+
|
| 35 |
+
console.log("addTextToVideo: outputVideoPath:", outputVideoPath)
|
| 36 |
+
return outputVideoPath
|
| 37 |
}
|
src/core/files/deleteFile.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
CHANGED
|
@@ -1,17 +1,11 @@
|
|
| 1 |
import { promises as fs } from "node:fs"
|
| 2 |
import path from "node:path"
|
|
|
|
| 3 |
|
| 4 |
export const deleteFilesWithName = async (dir: string, name: string, debug?: boolean) => {
|
| 5 |
for (const file of await fs.readdir(dir)) {
|
| 6 |
if (file.includes(name)) {
|
| 7 |
-
|
| 8 |
-
try {
|
| 9 |
-
await fs.unlink(filePath)
|
| 10 |
-
} catch (err) {
|
| 11 |
-
if (debug) {
|
| 12 |
-
console.error(`failed to unlink file in ${filePath}: ${err}`)
|
| 13 |
-
}
|
| 14 |
-
}
|
| 15 |
}
|
| 16 |
}
|
| 17 |
}
|
|
|
|
| 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 |
for (const file of await fs.readdir(dir)) {
|
| 7 |
if (file.includes(name)) {
|
| 8 |
+
await deleteFile(path.join(dir, file))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
}
|
| 11 |
}
|
src/core/files/getRandomDirectory.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { tmpdir } from "node:os"
|
| 2 |
+
import { join } from "node:path"
|
| 3 |
+
import { mkdtemp } from "node:fs/promises"
|
| 4 |
+
import { v4 as uuidv4 } from "uuid"
|
| 5 |
+
|
| 6 |
+
export async function getRandomDirectory(): Promise<string> {
|
| 7 |
+
return mkdtemp(join(tmpdir(), uuidv4()))
|
| 8 |
+
}
|
src/core/files/removeTmpFiles.mts
CHANGED
|
@@ -1,22 +1,18 @@
|
|
| 1 |
import { existsSync, promises as fs } from "node:fs"
|
| 2 |
|
| 3 |
-
import { keepTemporaryFiles } from "../config.mts"
|
| 4 |
-
|
| 5 |
// note: this function will never fail
|
| 6 |
export async function removeTemporaryFiles(filesPaths: string[]) {
|
| 7 |
try {
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
await fs.unlink(filePath)
|
| 14 |
-
}
|
| 15 |
-
} catch (err) {
|
| 16 |
-
//
|
| 17 |
}
|
| 18 |
-
})
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
} catch (err) {
|
| 21 |
// no big deal, except a bit of tmp file leak
|
| 22 |
// although.. if delete failed, it could also indicate
|
|
|
|
| 1 |
import { existsSync, promises as fs } from "node:fs"
|
| 2 |
|
|
|
|
|
|
|
| 3 |
// note: this function will never fail
|
| 4 |
export async function removeTemporaryFiles(filesPaths: string[]) {
|
| 5 |
try {
|
| 6 |
+
// Cleanup temporary files - you could choose to do this or leave it to the user
|
| 7 |
+
await Promise.all(filesPaths.map(async (filePath) => {
|
| 8 |
+
try {
|
| 9 |
+
if (existsSync(filePath)) {
|
| 10 |
+
await fs.rm(filePath)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
+
} catch (err) {
|
| 13 |
+
//
|
| 14 |
+
}
|
| 15 |
+
}))
|
| 16 |
} catch (err) {
|
| 17 |
// no big deal, except a bit of tmp file leak
|
| 18 |
// although.. if delete failed, it could also indicate
|
src/core/utils/startOfSegment1IsWithinSegment2.mts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ClapSegment } from "../clap/types.mts";
|
| 2 |
+
|
| 3 |
+
export function startOfSegment1IsWithinSegment2(s1: ClapSegment, s2: ClapSegment) {
|
| 4 |
+
const startOfSegment1 = s1.startTimeInMs
|
| 5 |
+
return s2.startTimeInMs <= startOfSegment1 && startOfSegment1 <= s2.endTimeInMs
|
| 6 |
+
}
|
src/index.mts
CHANGED
|
@@ -4,6 +4,8 @@ import { Blob } from "buffer"
|
|
| 4 |
|
| 5 |
import { parseClap } from "./core/clap/parseClap.mts"
|
| 6 |
import { ClapProject } from "./core/clap/types.mts"
|
|
|
|
|
|
|
| 7 |
|
| 8 |
const app = express()
|
| 9 |
const port = 7860
|
|
@@ -49,10 +51,23 @@ app.post("/", async (req, res) => {
|
|
| 49 |
req.on("end", async () => {
|
| 50 |
let clapProject: ClapProject
|
| 51 |
try {
|
| 52 |
-
let fileData = Buffer.concat(data)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
console.log("got a clap project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
} catch (err) {
|
| 57 |
console.error(`failed to parse the request: ${err}`)
|
| 58 |
res.status(500)
|
|
@@ -60,10 +75,6 @@ app.post("/", async (req, res) => {
|
|
| 60 |
res.end()
|
| 61 |
return
|
| 62 |
}
|
| 63 |
-
// TODO read the mp4 file and convert it to
|
| 64 |
-
res.status(200)
|
| 65 |
-
res.write("TODO")
|
| 66 |
-
res.end()
|
| 67 |
});
|
| 68 |
})
|
| 69 |
|
|
|
|
| 4 |
|
| 5 |
import { parseClap } from "./core/clap/parseClap.mts"
|
| 6 |
import { ClapProject } from "./core/clap/types.mts"
|
| 7 |
+
import { clapToTmpVideoFilePath } from "./main.mts"
|
| 8 |
+
import { deleteFile } from "./core/files/deleteFile.mts"
|
| 9 |
|
| 10 |
const app = express()
|
| 11 |
const port = 7860
|
|
|
|
| 51 |
req.on("end", async () => {
|
| 52 |
let clapProject: ClapProject
|
| 53 |
try {
|
| 54 |
+
let fileData = Buffer.concat(data)
|
| 55 |
+
|
| 56 |
+
const clap = await parseClap(new Blob([fileData]));
|
| 57 |
+
console.log("got a clap project:", clapProject)
|
| 58 |
+
|
| 59 |
+
const {
|
| 60 |
+
tmpWorkDir,
|
| 61 |
+
outputFilePath,
|
| 62 |
+
} = await clapToTmpVideoFilePath({ clap })
|
| 63 |
+
console.log("got an output file at:", outputFilePath)
|
| 64 |
+
|
| 65 |
+
res.download(outputFilePath, async () => {
|
| 66 |
+
// clean-up after ourselves (we clear the whole tmp directory)
|
| 67 |
+
await deleteFile(tmpWorkDir)
|
| 68 |
+
console.log("cleared the temporary folder")
|
| 69 |
+
})
|
| 70 |
+
return
|
| 71 |
} catch (err) {
|
| 72 |
console.error(`failed to parse the request: ${err}`)
|
| 73 |
res.status(500)
|
|
|
|
| 75 |
res.end()
|
| 76 |
return
|
| 77 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
});
|
| 79 |
})
|
| 80 |
|
src/main.mts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
| 1 |
-
import { tmpdir } from "node:os"
|
| 2 |
import { join } from "node:path"
|
| 3 |
-
import { mkdtemp } from "node:fs/promises"
|
| 4 |
-
import { v4 as uuidv4 } from "uuid"
|
| 5 |
|
| 6 |
import { ClapProject } from "./core/clap/types.mts";
|
| 7 |
import { concatenateAudio } from "./core/ffmpeg/concatenateAudio.mts";
|
|
@@ -9,6 +6,11 @@ import { concatenateVideosWithAudio } from "./core/ffmpeg/concatenateVideosWithA
|
|
| 9 |
import { writeBase64ToFile } from "./core/files/writeBase64ToFile.mts";
|
| 10 |
import { concatenateVideos } from "./core/ffmpeg/concatenateVideos.mts"
|
| 11 |
import { deleteFilesWithName } from "./core/files/deleteFileWithName.mts"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
/**
|
| 14 |
* Generate a .mp4 video inside a direcory (if none is provided, it will be created in /tmp)
|
|
@@ -16,56 +18,139 @@ import { deleteFilesWithName } from "./core/files/deleteFileWithName.mts"
|
|
| 16 |
* @param clap
|
| 17 |
* @returns file path to the final .mp4
|
| 18 |
*/
|
| 19 |
-
export async function clapToTmpVideoFilePath(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
const videoFilePaths: string[] = []
|
| 24 |
const videoSegments = clap.segments.filter(s => s.category === "video" && s.assetUrl.startsWith("data:video/"))
|
| 25 |
|
| 26 |
for (const segment of videoSegments) {
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
-
const
|
| 36 |
videoFilePaths,
|
| 37 |
-
output: join(
|
| 38 |
})
|
| 39 |
|
| 40 |
const audioTracks: string[] = []
|
| 41 |
|
| 42 |
-
const musicSegments = clap.segments.filter(s =>
|
|
|
|
|
|
|
|
|
|
| 43 |
for (const segment of musicSegments) {
|
| 44 |
audioTracks.push(
|
| 45 |
await writeBase64ToFile(
|
| 46 |
segment.assetUrl,
|
| 47 |
-
join(
|
| 48 |
)
|
| 49 |
)
|
| 50 |
}
|
| 51 |
|
| 52 |
const concatenatedAudio = await concatenateAudio({
|
| 53 |
-
output: join(
|
| 54 |
audioTracks,
|
| 55 |
crossfadeDurationInSec: 2 // 2 seconds
|
| 56 |
})
|
| 57 |
|
| 58 |
-
const
|
| 59 |
-
output: join(
|
| 60 |
audioFilePath: concatenatedAudio.filepath,
|
| 61 |
-
videoFilePaths: [
|
| 62 |
// videos are silent, so they can stay at 0
|
| 63 |
-
videoTracksVolume: 0.
|
| 64 |
-
audioTrackVolume:
|
| 65 |
})
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
return
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
|
|
|
|
|
|
| 1 |
import { join } from "node:path"
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import { ClapProject } from "./core/clap/types.mts";
|
| 4 |
import { concatenateAudio } from "./core/ffmpeg/concatenateAudio.mts";
|
|
|
|
| 6 |
import { writeBase64ToFile } from "./core/files/writeBase64ToFile.mts";
|
| 7 |
import { concatenateVideos } from "./core/ffmpeg/concatenateVideos.mts"
|
| 8 |
import { deleteFilesWithName } from "./core/files/deleteFileWithName.mts"
|
| 9 |
+
import { getRandomDirectory } from "./core/files/getRandomDirectory.mts";
|
| 10 |
+
import { addTextToVideo } from "./core/ffmpeg/addTextToVideo.mts";
|
| 11 |
+
import { startOfSegment1IsWithinSegment2 } from "./core/utils/startOfSegment1IsWithinSegment2.mts";
|
| 12 |
+
import { deleteFile } from "./core/files/deleteFile.mts";
|
| 13 |
+
import { extractBase64 } from "./core/base64/extractBase64.mts";
|
| 14 |
|
| 15 |
/**
|
| 16 |
* Generate a .mp4 video inside a direcory (if none is provided, it will be created in /tmp)
|
|
|
|
| 18 |
* @param clap
|
| 19 |
* @returns file path to the final .mp4
|
| 20 |
*/
|
| 21 |
+
export async function clapToTmpVideoFilePath({
|
| 22 |
+
clap,
|
| 23 |
+
outputDir = "",
|
| 24 |
+
clearTmpFilesAtEnd = false
|
| 25 |
+
}: {
|
| 26 |
+
clap: ClapProject
|
| 27 |
|
| 28 |
+
outputDir?: string
|
| 29 |
+
|
| 30 |
+
// if you leave this to false, you will have to clear files yourself
|
| 31 |
+
// (eg. after sending the final video file over)
|
| 32 |
+
clearTmpFilesAtEnd?: boolean
|
| 33 |
+
}): Promise<{
|
| 34 |
+
tmpWorkDir: string
|
| 35 |
+
outputFilePath: string
|
| 36 |
+
}> {
|
| 37 |
+
|
| 38 |
+
outputDir = outputDir || (await getRandomDirectory())
|
| 39 |
|
| 40 |
const videoFilePaths: string[] = []
|
| 41 |
const videoSegments = clap.segments.filter(s => s.category === "video" && s.assetUrl.startsWith("data:video/"))
|
| 42 |
|
| 43 |
for (const segment of videoSegments) {
|
| 44 |
+
|
| 45 |
+
const base64Info = extractBase64(segment.assetUrl)
|
| 46 |
+
|
| 47 |
+
// we write it to the disk *unconverted* (it might be a mp4, a webm or something else)
|
| 48 |
+
let videoSegmentFilePath = await writeBase64ToFile(
|
| 49 |
+
segment.assetUrl,
|
| 50 |
+
join(outputDir, `tmp_asset_${segment.id}.${base64Info.extension}`)
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
const interfaceSegments = clap.segments.filter(s =>
|
| 54 |
+
s.assetUrl.startsWith("data:text/") &&
|
| 55 |
+
s.category === "interface" &&
|
| 56 |
+
startOfSegment1IsWithinSegment2(s, segment)
|
| 57 |
+
)
|
| 58 |
+
const interfaceSegment = interfaceSegments.at(0)
|
| 59 |
+
if (interfaceSegment) {
|
| 60 |
+
// here we are free to use mp4, since this is an internal intermediary format
|
| 61 |
+
const videoSegmentWithOverlayFilePath = join(outputDir, `tmp_asset_${segment.id}_with_interface.mp4`)
|
| 62 |
+
|
| 63 |
+
await addTextToVideo({
|
| 64 |
+
inputVideoPath: videoSegmentFilePath,
|
| 65 |
+
outputVideoPath: videoSegmentWithOverlayFilePath,
|
| 66 |
+
text: atob(extractBase64(interfaceSegment.assetUrl).data),
|
| 67 |
+
width: clap.meta.width,
|
| 68 |
+
height: clap.meta.height,
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
// we overwrite
|
| 72 |
+
await deleteFile(videoSegmentFilePath)
|
| 73 |
+
videoSegmentFilePath = videoSegmentWithOverlayFilePath
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
const dialogueSegments = clap.segments.filter(s =>
|
| 77 |
+
s.assetUrl.startsWith("data:audio/") &&
|
| 78 |
+
s.category === "dialogue" &&
|
| 79 |
+
startOfSegment1IsWithinSegment2(s, segment)
|
| 80 |
)
|
| 81 |
+
const dialogueSegment = dialogueSegments.at(0)
|
| 82 |
+
if (dialogueSegment) {
|
| 83 |
+
extractBase64(dialogueSegment.assetUrl)
|
| 84 |
+
const base64Info = extractBase64(segment.assetUrl)
|
| 85 |
+
|
| 86 |
+
const dialogueSegmentFilePath = await writeBase64ToFile(
|
| 87 |
+
dialogueSegment.assetUrl,
|
| 88 |
+
join(outputDir, `tmp_asset_${segment.id}_dialogue.${base64Info.extension}`)
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
const finalFilePathOfVideoWithSound = await concatenateVideosWithAudio({
|
| 92 |
+
output: join(outputDir, `${segment.id}_video_with_audio.mp4`),
|
| 93 |
+
audioFilePath: dialogueSegmentFilePath,
|
| 94 |
+
videoFilePaths: [videoSegmentFilePath],
|
| 95 |
+
// videos are silent, so they can stay at 0
|
| 96 |
+
videoTracksVolume: 0.0,
|
| 97 |
+
audioTrackVolume: 1.0,
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
// we delete the temporary dialogue file
|
| 101 |
+
await deleteFile(dialogueSegmentFilePath)
|
| 102 |
+
|
| 103 |
+
// we overwrite the video segment
|
| 104 |
+
await deleteFile(videoSegmentFilePath)
|
| 105 |
+
|
| 106 |
+
videoSegmentFilePath = finalFilePathOfVideoWithSound
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
videoFilePaths.push(videoSegmentFilePath)
|
| 110 |
}
|
| 111 |
|
| 112 |
+
const concatenatedVideosNoMusic = await concatenateVideos({
|
| 113 |
videoFilePaths,
|
| 114 |
+
output: join(outputDir, `tmp_asset_concatenated_videos.mp4`)
|
| 115 |
})
|
| 116 |
|
| 117 |
const audioTracks: string[] = []
|
| 118 |
|
| 119 |
+
const musicSegments = clap.segments.filter(s =>
|
| 120 |
+
s.category === "music" &&
|
| 121 |
+
s.assetUrl.startsWith("data:audio/")
|
| 122 |
+
)
|
| 123 |
for (const segment of musicSegments) {
|
| 124 |
audioTracks.push(
|
| 125 |
await writeBase64ToFile(
|
| 126 |
segment.assetUrl,
|
| 127 |
+
join(outputDir, `tmp_asset_${segment.id}.wav`)
|
| 128 |
)
|
| 129 |
)
|
| 130 |
}
|
| 131 |
|
| 132 |
const concatenatedAudio = await concatenateAudio({
|
| 133 |
+
output: join(outputDir, `tmp_asset_concatenated_audio.wav`),
|
| 134 |
audioTracks,
|
| 135 |
crossfadeDurationInSec: 2 // 2 seconds
|
| 136 |
})
|
| 137 |
|
| 138 |
+
const finalFilePathOfVideoWithMusic = await concatenateVideosWithAudio({
|
| 139 |
+
output: join(outputDir, `final_video.mp4`),
|
| 140 |
audioFilePath: concatenatedAudio.filepath,
|
| 141 |
+
videoFilePaths: [concatenatedVideosNoMusic.filepath],
|
| 142 |
// videos are silent, so they can stay at 0
|
| 143 |
+
videoTracksVolume: 0.85,
|
| 144 |
+
audioTrackVolume: 0.15, // let's keep the music volume low
|
| 145 |
})
|
| 146 |
|
| 147 |
+
if (clearTmpFilesAtEnd) {
|
| 148 |
+
// we delete all the temporary assets
|
| 149 |
+
await deleteFilesWithName(outputDir, `tmp_asset_`)
|
| 150 |
+
}
|
| 151 |
|
| 152 |
+
return {
|
| 153 |
+
tmpWorkDir: outputDir,
|
| 154 |
+
outputFilePath: finalFilePathOfVideoWithMusic
|
| 155 |
+
}
|
| 156 |
}
|