Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 8,390 Bytes
2cae2a9 93a2c43 3165afb d5d9687 3165afb 2cae2a9 3165afb d5d9687 7fca805 4cb7ad9 2cae2a9 2ed47da 2cae2a9 46fcec6 4cb7ad9 46fcec6 2cae2a9 4cb7ad9 46fcec6 74dba9a 46fcec6 1083ad0 46fcec6 2cae2a9 7fca805 a9505f0 93a2c43 2ed47da d5d9687 1f38dc1 2ed47da 1f38dc1 2ed47da b80b9f7 26058e7 b80b9f7 1f38dc1 2ed47da 1f38dc1 2ed47da b80b9f7 26058e7 b80b9f7 1f38dc1 2ed47da 2cae2a9 1083ad0 6349b58 46fcec6 2cae2a9 46fcec6 2cae2a9 6349b58 74dba9a 2cae2a9 46fcec6 93a2c43 46fcec6 6349b58 d5d9687 2cae2a9 d5d9687 74dba9a d5d9687 2cae2a9 d5d9687 74dba9a d5d9687 2cae2a9 fc91769 74dba9a fc91769 d5d9687 74dba9a d5d9687 74dba9a d5d9687 74dba9a d5d9687 74dba9a d5d9687 74dba9a d5d9687 74dba9a d5d9687 74dba9a fc91769 d5d9687 74dba9a d5d9687 fc91769 74dba9a fc91769 74dba9a 1083ad0 74dba9a 1083ad0 46fcec6 4cb7ad9 fc91769 46fcec6 2cae2a9 fc91769 2cae2a9 1083ad0 6349b58 46fcec6 74dba9a 46fcec6 2cae2a9 74dba9a 6349b58 74dba9a 6349b58 46fcec6 2cae2a9 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
import { join } from "node:path"
import { ClapProject, ClapSegmentCategory } from "@aitube/clap"
import { deleteFilesWithName, getRandomDirectory, writeBase64ToFile } from "@aitube/io"
import {
concatenateAudio,
concatenateVideos,
concatenateVideosWithAudio,
defaultExportFormat,
type SupportedExportFormat,
type ConcatenateAudioOutput,
getMediaInfo
// } from "@aitube/ffmpeg"
} from "./bug-in-bun/aitube_ffmpeg"
import { clapWithStoryboardsToVideoFile } from "./core/exporters/clapWithStoryboardsToVideoFile"
import { clapWithVideosToVideoFile } from "./core/exporters/clapWithVideosToVideoFile"
import { extractBase64 } from "@aitube/encoders"
import { videoChunksOnly } from "./core/utils/videoChunksOnly"
/**
* Generate a .mp4 video inside a directory (if none is provided, it will be created in /tmp)
*
* @param clap
* @returns file path to the final .mp4
*/
export async function clapToTmpVideoFilePath({
clap,
format = defaultExportFormat,
outputDir = "",
clearTmpFilesAtEnd = false
}: {
clap: ClapProject
format?: SupportedExportFormat
outputDir?: string
// if you leave this to false, you will have to clear files yourself
// (eg. after sending the final video file over)
clearTmpFilesAtEnd?: boolean
}): Promise<{
tmpWorkDir: string
outputFilePath: string
}> {
console.log(`clapToTmpVideoFilePath()`)
// in case we have an issue with the format
if (format !== "mp4" && format !== "webm") {
format = "mp4"
}
outputDir = outputDir || (await getRandomDirectory())
const videoSegments = videoChunksOnly(clap)
const storyboardSegments = clap.segments.filter(s => s.category === ClapSegmentCategory.STORYBOARD && s.assetUrl.startsWith("data:image/"))
const canUseVideos = videoSegments.length > 0
const canUseStoryboards = !canUseVideos && storyboardSegments.length > 0
// we count the duration of the whole video
let totalDurationInMs = 0
clap.segments.forEach(s => {
if (s.endTimeInMs > totalDurationInMs) {
totalDurationInMs = s.endTimeInMs
}
})
let videoFilePaths: string[] = []
// two possibilities:
// we can either generate from the video files, or from the storyboards
// the storyboard video will be a bit more boring, but at least it should process faster
if (canUseVideos) {
const concatenatedData = await clapWithVideosToVideoFile({
clap,
videoSegments,
outputDir,
})
// console.log(`clapToTmpVideoFilePath: called clapWithVideosToVideoFile, got concatenatedData = ${JSON.stringify(concatenatedData, null, 2)}`)
videoFilePaths = concatenatedData.videoFilePaths
} else if (canUseStoryboards) {
const concatenatedData = await clapWithStoryboardsToVideoFile({
clap,
storyboardSegments,
outputDir,
})
// console.log(`clapToTmpVideoFilePath: called clapWithStoryboardsToVideoFile, got concatenatedData = ${JSON.stringify(concatenatedData, null, 2)}`)
videoFilePaths = concatenatedData.videoFilePaths
} else {
throw new Error(`the provided Clap doesn't contain any video or storyboard`)
}
console.log(`clapToTmpVideoFilePath: calling concatenateVideos over ${videoFilePaths.length} video chunks: ${JSON.stringify(videoFilePaths, null, 2)}\nconcatenateVideos(${JSON.stringify({
videoFilePaths,
output: join(outputDir, `tmp_asset_concatenated_videos.mp4`)
}, null, 2)})`)
const concatenatedVideosNoMusic = await concatenateVideos({
videoFilePaths,
output: join(outputDir, `tmp_asset_concatenated_videos.mp4`)
})
console.log(`clapToTmpVideoFilePath: concatenatedVideosNoMusic`, concatenatedVideosNoMusic)
const musicFilePaths: string[] = []
const musicSegments = clap.segments.filter(s =>
s.category === ClapSegmentCategory.MUSIC &&
s.assetUrl.startsWith("data:audio/")
)
console.log(`clapToTmpVideoFilePath: got ${musicSegments.length} music segments in total`)
// note: once we start with a certain type eg. mp3, there is no going to back
// another format like wav, we can't concatenate them together (well, not yet)
let detectedMusicTrackFormat = ''
// we count how much music has been generated
// if it is not enough to fill the full video, we will loop it (using cross-fading)
let availableMusicDurationInMs = 0
for (const segment of musicSegments) {
const analysis = extractBase64(segment.assetUrl)
if (!detectedMusicTrackFormat) {
detectedMusicTrackFormat = analysis.extension
} else if (detectedMusicTrackFormat !== analysis.extension) {
throw new Error(`fatal error: concatenating a mixture of ${detectedMusicTrackFormat} and ${analysis.extension} tracks isn't supported yet`)
}
const { durationInMs, hasAudio } = await getMediaInfo(segment.assetUrl)
// we have to skip silent music tracks
if (!hasAudio) {
console.log(`skipping a silent music track`)
continue
}
const newMusicFilePath = await writeBase64ToFile(
segment.assetUrl,
join(outputDir, `tmp_asset_${segment.id}.${analysis.extension}`)
)
// console.log("wrote music to " + newMusicFilePath)
musicFilePaths.push(newMusicFilePath)
availableMusicDurationInMs += durationInMs
}
let concatenatedAudio: ConcatenateAudioOutput | undefined = undefined
if (musicFilePaths.length > 0) {
// console.log(`clapToTmpVideoFilePath: calling concatenateAudio over ${musicFilePaths.length} audio tracks`)
if (!detectedMusicTrackFormat) {
throw new Error(`uh that's weird, we couldn't detect the audio type`)
}
const availableMusicFilePaths = [...musicFilePaths]
// if we don't have enough music audio content
while (availableMusicDurationInMs < totalDurationInMs) {
let musicFilePathToRepeat = availableMusicFilePaths.shift()
// abort if there are no available tracks (for some reason)
if (!musicFilePathToRepeat) { break }
availableMusicFilePaths.push(musicFilePathToRepeat)
// we artificially duplicate it (note: this will be cross-faded)
const { durationInMs } = await getMediaInfo(musicFilePathToRepeat)
// let's abord if we have bad data
if (!durationInMs || durationInMs < 1000) { break }
musicFilePaths.push(musicFilePathToRepeat)
availableMusicDurationInMs += durationInMs
}
/*
console.log("DEBUG:", {
musicFilePaths,
availableMusicFilePaths,
availableMusicDurationInMs
})
*/
concatenatedAudio = await concatenateAudio({
output: join(outputDir, `tmp_asset_concatenated_audio.${detectedMusicTrackFormat}`),
audioFilePaths: musicFilePaths,
crossfadeDurationInSec: 2, // 2 seconds
outputFormat: detectedMusicTrackFormat
})
// console.log(`clapToTmpVideoFilePath: concatenatedAudio = ${concatenatedAudio}`)
}
/*
console.log(`calling concatenateVideosWithAudio: `, {
output: join(outputDir, `final_video.${format}`),
format,
audioFilePath: concatenatedAudio ? concatenatedAudio?.filepath : undefined,
videoFilePaths: [concatenatedVideosNoMusic.filepath],
// videos are silent, so they can stay at 0
videoTracksVolume: concatenatedAudio ? 0.85 : 1.0,
audioTrackVolume: concatenatedAudio ? 0.15 : 0.0, // let's keep the music volume low
})
*/
const finalFilePathOfVideoWithMusic = await concatenateVideosWithAudio({
output: join(outputDir, `final_video.${format}`),
format,
audioFilePath: concatenatedAudio ? concatenatedAudio?.filepath : undefined,
videoFilePaths: [concatenatedVideosNoMusic.filepath],
// videos are silent, so they can stay at 0
videoTracksVolume: concatenatedAudio ? 0.85 : 1.0,
audioTrackVolume: concatenatedAudio ? 0.15 : 0.0, // let's keep the music volume low
})
console.log(`clapToTmpVideoFilePath: finalFilePathOfVideoWithMusic = ${finalFilePathOfVideoWithMusic}`)
if (clearTmpFilesAtEnd) {
// we delete all the temporary assets
// console.log(`clapToTmpVideoFilePath: calling deleteFilesWithName(${outputDir}, 'tmp_asset_')`)
await deleteFilesWithName(outputDir, `tmp_asset_`)
}
/*
console.log(`clapToTmpVideoFilePath: returning ${JSON.stringify( {
tmpWorkDir: outputDir,
outputFilePath: finalFilePathOfVideoWithMusic
}, null, 2)}`)
*/
return {
tmpWorkDir: outputDir,
outputFilePath: finalFilePathOfVideoWithMusic
}
} |