jbilcke-hf HF staff commited on
Commit
44b5f05
·
1 Parent(s): a73c130

try to do things in parallel

Browse files
src/core/exporters/clapWithStoryboardsToVideoFile.mts CHANGED
@@ -1,15 +1,7 @@
1
- import { join } from "node:path"
2
-
3
  import { ClapProject, ClapSegment } from "@aitube/clap"
4
 
5
- import { concatenateVideosWithAudio } from "../ffmpeg/concatenateVideosWithAudio.mts"
6
- import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"
7
  import { getRandomDirectory } from "../files/getRandomDirectory.mts"
8
- import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts"
9
- import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts"
10
- import { deleteFile } from "../files/deleteFile.mts"
11
- import { extractBase64 } from "../base64/extractBase64.mts"
12
- import { imageToVideoBase64 } from "../ffmpeg/imageToVideoBase64.mts"
13
 
14
  export async function clapWithStoryboardsToVideoFile({
15
  clap,
@@ -26,91 +18,13 @@ export async function clapWithStoryboardsToVideoFile({
26
 
27
  outputDir = outputDir || (await getRandomDirectory())
28
 
29
- const videoFilePaths: string[] = []
30
-
31
- for (const segment of storyboardSegments) {
32
-
33
- let storyboardSegmentVideoFilePath = join(outputDir, `tmp_asset_${segment.id}_as_video.mp4`)
34
-
35
- await imageToVideoBase64({
36
- inputImageInBase64: segment.assetUrl,
37
- outputFilePath: storyboardSegmentVideoFilePath,
38
- width: clap.meta.width,
39
- height: clap.meta.height,
40
- outputVideoDurationInMs: 5000, // TODO this should be computed from the voice? or we can resize videos, toos
41
  outputDir,
42
- clearOutputDirAtTheEnd: false, // <- must stay false or else we lose everything!
43
- outputVideoFormat: "mp4",
44
  })
45
-
46
- const interfaceSegments = clap.segments.filter(s =>
47
- // nope, not all interfaces asset have the assetUrl
48
- // although in the future.. we might want to
49
- // s.assetUrl.startsWith("data:text/") &&
50
- s.category === "interface" &&
51
- startOfSegment1IsWithinSegment2(s, segment)
52
- )
53
- console.log(`clapWithStoryboardsToVideoFile: got ${interfaceSegments.length} interface segments for shot ${segment.id} [${segment.startTimeInMs}:${segment.endTimeInMs}]`)
54
-
55
- const interfaceSegment = interfaceSegments.at(0)
56
- if (interfaceSegment) {
57
- // here we are free to use mp4, since this is an internal intermediary format
58
- const videoSegmentWithOverlayFilePath = join(outputDir, `tmp_asset_${segment.id}_with_interface.mp4`)
59
-
60
- await addTextToVideo({
61
- inputVideoPath: storyboardSegmentVideoFilePath,
62
- outputVideoPath: videoSegmentWithOverlayFilePath,
63
- text: interfaceSegment.assetUrl.startsWith("data:text/")
64
- ? atob(extractBase64(interfaceSegment.assetUrl).data)
65
- : interfaceSegment.assetUrl,
66
- width: clap.meta.width,
67
- height: clap.meta.height,
68
- })
69
-
70
- // we overwrite
71
- await deleteFile(storyboardSegmentVideoFilePath)
72
- storyboardSegmentVideoFilePath = videoSegmentWithOverlayFilePath
73
- }
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
-
82
- console.log(`clapWithStoryboardsToVideoFile: got ${dialogueSegments.length} dialogue segments for shot ${segment.id} [${segment.startTimeInMs}:${segment.endTimeInMs}]`)
83
-
84
- const dialogueSegment = dialogueSegments.at(0)
85
- if (dialogueSegment) {
86
- extractBase64(dialogueSegment.assetUrl)
87
- const base64Info = extractBase64(dialogueSegment.assetUrl)
88
-
89
- const dialogueSegmentFilePath = await writeBase64ToFile(
90
- dialogueSegment.assetUrl,
91
- join(outputDir, `tmp_asset_${segment.id}_dialogue.${base64Info.extension}`)
92
- )
93
-
94
- const finalFilePathOfVideoWithSound = await concatenateVideosWithAudio({
95
- output: join(outputDir, `${segment.id}_video_with_audio.mp4`),
96
- audioFilePath: dialogueSegmentFilePath,
97
- videoFilePaths: [storyboardSegmentVideoFilePath],
98
- // videos are silent, so they can stay at 0
99
- videoTracksVolume: 0.0,
100
- audioTrackVolume: 1.0,
101
- })
102
-
103
- // we delete the temporary dialogue file
104
- await deleteFile(dialogueSegmentFilePath)
105
-
106
- // we overwrite the video segment
107
- await deleteFile(storyboardSegmentVideoFilePath)
108
-
109
- storyboardSegmentVideoFilePath = finalFilePathOfVideoWithSound
110
- }
111
-
112
- videoFilePaths.push(storyboardSegmentVideoFilePath)
113
- }
114
 
115
  // console.log(`clapWithStoryboardsToVideoFile: videoFilePaths: ${JSON.stringify(videoFilePaths, null, 2)}`)
116
 
 
 
 
1
  import { ClapProject, ClapSegment } from "@aitube/clap"
2
 
 
 
3
  import { getRandomDirectory } from "../files/getRandomDirectory.mts"
4
+ import { storyboardSegmentToVideoFile } from "./storyboardSegmentToVideoFile.mts"
 
 
 
 
5
 
6
  export async function clapWithStoryboardsToVideoFile({
7
  clap,
 
18
 
19
  outputDir = outputDir || (await getRandomDirectory())
20
 
21
+ const videoFilePaths: string[] = await Promise.all(storyboardSegments.map(segment =>
22
+ storyboardSegmentToVideoFile({
23
+ clap,
24
+ segment,
 
 
 
 
 
 
 
 
25
  outputDir,
 
 
26
  })
27
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  // console.log(`clapWithStoryboardsToVideoFile: videoFilePaths: ${JSON.stringify(videoFilePaths, null, 2)}`)
30
 
src/core/exporters/clapWithVideosToVideoFile.mts CHANGED
@@ -1,14 +1,7 @@
1
- import { join } from "node:path"
2
-
3
  import { ClapProject, ClapSegment } from "@aitube/clap"
4
 
5
- import { concatenateVideosWithAudio } from "../ffmpeg/concatenateVideosWithAudio.mts"
6
- import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"
7
  import { getRandomDirectory } from "../files/getRandomDirectory.mts"
8
- import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts"
9
- import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts"
10
- import { deleteFile } from "../files/deleteFile.mts"
11
- import { extractBase64 } from "../base64/extractBase64.mts"
12
 
13
 
14
  export async function clapWithVideosToVideoFile({
@@ -26,80 +19,13 @@ export async function clapWithVideosToVideoFile({
26
 
27
  outputDir = outputDir || (await getRandomDirectory())
28
 
29
- const videoFilePaths: string[] = []
30
-
31
- for (const segment of videoSegments) {
32
-
33
- const base64Info = extractBase64(segment.assetUrl)
34
-
35
- // we write it to the disk *unconverted* (it might be a mp4, a webm or something else)
36
- let videoSegmentFilePath = await writeBase64ToFile(
37
- segment.assetUrl,
38
- join(outputDir, `tmp_asset_${segment.id}.${base64Info.extension}`)
39
- )
40
-
41
- const interfaceSegments = clap.segments.filter(s =>
42
- // nope, not all interfaces asset have the assetUrl
43
- // although in the future.. we might want to
44
- // s.assetUrl.startsWith("data:text/") &&
45
- s.category === "interface" &&
46
- startOfSegment1IsWithinSegment2(s, segment)
47
- )
48
- const interfaceSegment = interfaceSegments.at(0)
49
- if (interfaceSegment) {
50
- // here we are free to use mp4, since this is an internal intermediary format
51
- const videoSegmentWithOverlayFilePath = join(outputDir, `tmp_asset_${segment.id}_with_interface.mp4`)
52
-
53
- await addTextToVideo({
54
- inputVideoPath: videoSegmentFilePath,
55
- outputVideoPath: videoSegmentWithOverlayFilePath,
56
- text: interfaceSegment.assetUrl.startsWith("data:text/")
57
- ? atob(extractBase64(interfaceSegment.assetUrl).data)
58
- : interfaceSegment.assetUrl,
59
- width: clap.meta.width,
60
- height: clap.meta.height,
61
- })
62
-
63
- // we overwrite
64
- await deleteFile(videoSegmentFilePath)
65
- videoSegmentFilePath = videoSegmentWithOverlayFilePath
66
- }
67
-
68
- const dialogueSegments = clap.segments.filter(s =>
69
- s.assetUrl.startsWith("data:audio/") &&
70
- s.category === "dialogue" &&
71
- startOfSegment1IsWithinSegment2(s, segment)
72
- )
73
- const dialogueSegment = dialogueSegments.at(0)
74
- if (dialogueSegment) {
75
- extractBase64(dialogueSegment.assetUrl)
76
- const base64Info = extractBase64(dialogueSegment.assetUrl)
77
-
78
- const dialogueSegmentFilePath = await writeBase64ToFile(
79
- dialogueSegment.assetUrl,
80
- join(outputDir, `tmp_asset_${segment.id}_dialogue.${base64Info.extension}`)
81
- )
82
-
83
- const finalFilePathOfVideoWithSound = await concatenateVideosWithAudio({
84
- output: join(outputDir, `${segment.id}_video_with_audio.mp4`),
85
- audioFilePath: dialogueSegmentFilePath,
86
- videoFilePaths: [videoSegmentFilePath],
87
- // videos are silent, so they can stay at 0
88
- videoTracksVolume: 0.0,
89
- audioTrackVolume: 1.0,
90
- })
91
-
92
- // we delete the temporary dialogue file
93
- await deleteFile(dialogueSegmentFilePath)
94
-
95
- // we overwrite the video segment
96
- await deleteFile(videoSegmentFilePath)
97
-
98
- videoSegmentFilePath = finalFilePathOfVideoWithSound
99
- }
100
-
101
- videoFilePaths.push(videoSegmentFilePath)
102
- }
103
 
104
  console.log(`clapWithVideosToVideoFile: videoFilePaths: ${JSON.stringify(videoFilePaths, null, 2)}`)
105
 
 
 
 
1
  import { ClapProject, ClapSegment } from "@aitube/clap"
2
 
 
 
3
  import { getRandomDirectory } from "../files/getRandomDirectory.mts"
4
+ import { videoSegmentToVideoFile } from "./videoSegmentToVideoFile.mts"
 
 
 
5
 
6
 
7
  export async function clapWithVideosToVideoFile({
 
19
 
20
  outputDir = outputDir || (await getRandomDirectory())
21
 
22
+ const videoFilePaths: string[] = await Promise.all(videoSegments.map(segment =>
23
+ videoSegmentToVideoFile({
24
+ clap,
25
+ segment,
26
+ outputDir,
27
+ })
28
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  console.log(`clapWithVideosToVideoFile: videoFilePaths: ${JSON.stringify(videoFilePaths, null, 2)}`)
31
 
src/core/exporters/storyboardSegmentToVideoFile.mts ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { join } from "node:path"
2
+
3
+ import { ClapProject, ClapSegment } from "@aitube/clap"
4
+
5
+ import { concatenateVideosWithAudio } from "../ffmpeg/concatenateVideosWithAudio.mts"
6
+ import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"
7
+ import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts"
8
+ import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts"
9
+ import { deleteFile } from "../files/deleteFile.mts"
10
+ import { extractBase64 } from "../base64/extractBase64.mts"
11
+ import { imageToVideoBase64 } from "../ffmpeg/imageToVideoBase64.mts"
12
+
13
+ export async function storyboardSegmentToVideoFile({
14
+ clap,
15
+ segment,
16
+ outputDir,
17
+ }: {
18
+ clap: ClapProject
19
+ segment: ClapSegment
20
+ outputDir: string
21
+ }): Promise<string> {
22
+
23
+ let storyboardSegmentVideoFilePath = join(outputDir, `tmp_asset_${segment.id}_as_video.mp4`)
24
+
25
+ await imageToVideoBase64({
26
+ inputImageInBase64: segment.assetUrl,
27
+ outputFilePath: storyboardSegmentVideoFilePath,
28
+ width: clap.meta.width,
29
+ height: clap.meta.height,
30
+ outputVideoDurationInMs: 5000, // TODO this should be computed from the voice? or we can resize videos, toos
31
+ outputDir,
32
+ clearOutputDirAtTheEnd: false, // <- must stay false or else we lose everything!
33
+ outputVideoFormat: "mp4",
34
+ })
35
+
36
+ const interfaceSegments = clap.segments.filter(s =>
37
+ // nope, not all interfaces asset have the assetUrl
38
+ // although in the future.. we might want to
39
+ // s.assetUrl.startsWith("data:text/") &&
40
+ s.category === "interface" &&
41
+ startOfSegment1IsWithinSegment2(s, segment)
42
+ )
43
+ console.log(`clapWithStoryboardsToVideoFile: got ${interfaceSegments.length} interface segments for shot ${segment.id} [${segment.startTimeInMs}:${segment.endTimeInMs}]`)
44
+
45
+ const interfaceSegment = interfaceSegments.at(0)
46
+ if (interfaceSegment) {
47
+ // here we are free to use mp4, since this is an internal intermediary format
48
+ const videoSegmentWithOverlayFilePath = join(outputDir, `tmp_asset_${segment.id}_with_interface.mp4`)
49
+
50
+ await addTextToVideo({
51
+ inputVideoPath: storyboardSegmentVideoFilePath,
52
+ outputVideoPath: videoSegmentWithOverlayFilePath,
53
+ text: interfaceSegment.assetUrl.startsWith("data:text/")
54
+ ? atob(extractBase64(interfaceSegment.assetUrl).data)
55
+ : interfaceSegment.assetUrl,
56
+ width: clap.meta.width,
57
+ height: clap.meta.height,
58
+ })
59
+
60
+ // we overwrite
61
+ await deleteFile(storyboardSegmentVideoFilePath)
62
+ storyboardSegmentVideoFilePath = videoSegmentWithOverlayFilePath
63
+ }
64
+
65
+
66
+ const dialogueSegments = clap.segments.filter(s =>
67
+ s.assetUrl.startsWith("data:audio/") &&
68
+ s.category === "dialogue" &&
69
+ startOfSegment1IsWithinSegment2(s, segment)
70
+ )
71
+
72
+ console.log(`clapWithStoryboardsToVideoFile: got ${dialogueSegments.length} dialogue segments for shot ${segment.id} [${segment.startTimeInMs}:${segment.endTimeInMs}]`)
73
+
74
+ const dialogueSegment = dialogueSegments.at(0)
75
+ if (dialogueSegment) {
76
+ extractBase64(dialogueSegment.assetUrl)
77
+ const base64Info = extractBase64(dialogueSegment.assetUrl)
78
+
79
+ const dialogueSegmentFilePath = await writeBase64ToFile(
80
+ dialogueSegment.assetUrl,
81
+ join(outputDir, `tmp_asset_${segment.id}_dialogue.${base64Info.extension}`)
82
+ )
83
+
84
+ const finalFilePathOfVideoWithSound = await concatenateVideosWithAudio({
85
+ output: join(outputDir, `${segment.id}_video_with_audio.mp4`),
86
+ audioFilePath: dialogueSegmentFilePath,
87
+ videoFilePaths: [storyboardSegmentVideoFilePath],
88
+ // videos are silent, so they can stay at 0
89
+ videoTracksVolume: 0.0,
90
+ audioTrackVolume: 1.0,
91
+ })
92
+
93
+ // we delete the temporary dialogue file
94
+ await deleteFile(dialogueSegmentFilePath)
95
+
96
+ // we overwrite the video segment
97
+ await deleteFile(storyboardSegmentVideoFilePath)
98
+
99
+ storyboardSegmentVideoFilePath = finalFilePathOfVideoWithSound
100
+ }
101
+
102
+ return storyboardSegmentVideoFilePath
103
+ }
src/core/exporters/videoSegmentToVideoFile.mts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { join } from "node:path"
2
+
3
+ import { ClapProject, ClapSegment } from "@aitube/clap"
4
+
5
+ import { concatenateVideosWithAudio } from "../ffmpeg/concatenateVideosWithAudio.mts"
6
+ import { writeBase64ToFile } from "../files/writeBase64ToFile.mts"
7
+ import { addTextToVideo } from "../ffmpeg/addTextToVideo.mts"
8
+ import { startOfSegment1IsWithinSegment2 } from "../utils/startOfSegment1IsWithinSegment2.mts"
9
+ import { deleteFile } from "../files/deleteFile.mts"
10
+ import { extractBase64 } from "../base64/extractBase64.mts"
11
+
12
+
13
+ export async function videoSegmentToVideoFile({
14
+ clap,
15
+ segment,
16
+ outputDir,
17
+ }: {
18
+ clap: ClapProject
19
+ segment: ClapSegment
20
+ outputDir: string
21
+ }): Promise<string> {
22
+
23
+ const base64Info = extractBase64(segment.assetUrl)
24
+
25
+ // we write it to the disk *unconverted* (it might be a mp4, a webm or something else)
26
+ let videoSegmentFilePath = await writeBase64ToFile(
27
+ segment.assetUrl,
28
+ join(outputDir, `tmp_asset_${segment.id}.${base64Info.extension}`)
29
+ )
30
+ const interfaceSegments = clap.segments.filter(s =>
31
+ // nope, not all interfaces asset have the assetUrl
32
+ // although in the future.. we might want to
33
+ // s.assetUrl.startsWith("data:text/") &&
34
+ s.category === "interface" &&
35
+ startOfSegment1IsWithinSegment2(s, segment)
36
+ )
37
+ const interfaceSegment = interfaceSegments.at(0)
38
+ if (interfaceSegment) {
39
+ // here we are free to use mp4, since this is an internal intermediary format
40
+ const videoSegmentWithOverlayFilePath = join(outputDir, `tmp_asset_${segment.id}_with_interface.mp4`)
41
+ await addTextToVideo({
42
+ inputVideoPath: videoSegmentFilePath,
43
+ outputVideoPath: videoSegmentWithOverlayFilePath,
44
+ text: interfaceSegment.assetUrl.startsWith("data:text/")
45
+ ? atob(extractBase64(interfaceSegment.assetUrl).data)
46
+ : interfaceSegment.assetUrl,
47
+ width: clap.meta.width,
48
+ height: clap.meta.height,
49
+ })
50
+ // we overwrite
51
+ await deleteFile(videoSegmentFilePath)
52
+ videoSegmentFilePath = videoSegmentWithOverlayFilePath
53
+ }
54
+ const dialogueSegments = clap.segments.filter(s =>
55
+ s.assetUrl.startsWith("data:audio/") &&
56
+ s.category === "dialogue" &&
57
+ startOfSegment1IsWithinSegment2(s, segment)
58
+ )
59
+ const dialogueSegment = dialogueSegments.at(0)
60
+ if (dialogueSegment) {
61
+ extractBase64(dialogueSegment.assetUrl)
62
+ const base64Info = extractBase64(dialogueSegment.assetUrl)
63
+ const dialogueSegmentFilePath = await writeBase64ToFile(
64
+ dialogueSegment.assetUrl,
65
+ join(outputDir, `tmp_asset_${segment.id}_dialogue.${base64Info.extension}`)
66
+ )
67
+
68
+ const finalFilePathOfVideoWithSound = await concatenateVideosWithAudio({
69
+ output: join(outputDir, `${segment.id}_video_with_audio.mp4`),
70
+ audioFilePath: dialogueSegmentFilePath,
71
+ videoFilePaths: [videoSegmentFilePath],
72
+ // videos are silent, so they can stay at 0
73
+ videoTracksVolume: 0.0,
74
+ audioTrackVolume: 1.0,
75
+ })
76
+ // we delete the temporary dialogue file
77
+ await deleteFile(dialogueSegmentFilePath)
78
+ // we overwrite the video segment
79
+ await deleteFile(videoSegmentFilePath)
80
+ videoSegmentFilePath = finalFilePathOfVideoWithSound
81
+ }
82
+
83
+ return videoSegmentFilePath
84
+ }