jbilcke-hf HF staff commited on
Commit
5dfc565
Β·
1 Parent(s): deae345

ok! now, time to debug and build the frontend..

Browse files
Files changed (46) hide show
  1. .gitignore +0 -2
  2. database/completed/README.md +0 -0
  3. database/pending/README.md +0 -1
  4. package.json +1 -1
  5. src/config.mts +3 -3
  6. src/index.mts +34 -6
  7. src/initFolders.mts +12 -0
  8. src/main.mts +6 -2
  9. src/{services β†’ production}/addAudioToVideo.mts +6 -8
  10. src/production/assembleShots.mts +53 -0
  11. src/{services β†’ production}/generateActor.mts +0 -0
  12. src/{services β†’ production}/generateAudio.mts +13 -11
  13. src/{services β†’ production}/generateAudioLegacy.mts +1 -1
  14. src/{services β†’ production}/generateShot.mts +5 -5
  15. src/{services β†’ production}/generateVideo.mts +1 -1
  16. src/{services β†’ production}/generateVoice.mts +2 -2
  17. src/{services β†’ production}/interpolateVideo.mts +13 -6
  18. src/{services β†’ production}/interpolateVideoLegacy.mts +2 -2
  19. src/{services β†’ production}/mergeAudio.mts +0 -0
  20. src/{services β†’ production}/postInterpolation.mts +12 -12
  21. src/{services β†’ production}/upscaleVideo.mts +15 -25
  22. src/scheduler/deleteTask.mts +33 -0
  23. src/{database β†’ scheduler}/getCompletedTasks.mts +1 -1
  24. src/{database β†’ scheduler}/getPendingTasks.mts +1 -1
  25. src/{database β†’ scheduler}/getTask.mts +1 -1
  26. src/scheduler/processTask.mts +246 -0
  27. src/{database β†’ scheduler}/readTask.mts +0 -0
  28. src/{database β†’ scheduler}/readTasks.mts +0 -0
  29. src/{database β†’ scheduler}/saveCompletedTask.mts +2 -2
  30. src/{database β†’ scheduler}/savePendingTask.mts +0 -0
  31. src/{database β†’ scheduler}/updatePendingTask.mts +0 -0
  32. src/services/processTask.mts +0 -68
  33. src/types.mts +12 -18
  34. src/utils/copyVideoFromPendingToCompleted.mts +15 -0
  35. src/utils/copyVideoFromTmpToCompleted.mts +20 -0
  36. src/utils/copyVideoFromTmpToPending.mts +21 -0
  37. src/utils/createDirIfNeeded.mts +7 -0
  38. src/utils/deleteFileIfExists.mts +13 -0
  39. src/{services/downloadVideo.mts β†’ utils/downloadFileToTmp.mts} +7 -8
  40. src/{services β†’ utils}/generateSeed.mts +0 -0
  41. src/utils/moveFile.mts +15 -0
  42. src/utils/moveFileFromTmpToPending.mts +18 -0
  43. src/utils/moveVideoFromPendingToCompleted.mts +14 -0
  44. src/utils/moveVideoFromTmpToCompleted.mts +18 -0
  45. src/utils/parseShotRequest.mts +9 -5
  46. src/utils/parseVideoRequest.mts +10 -5
.gitignore CHANGED
@@ -3,7 +3,5 @@ node_modules
3
  *.bin
4
  .DS_Store
5
  .venv
6
- ./database/completed/*.json
7
- ./database/pending/*.json
8
  *.mp4
9
  sandbox
 
3
  *.bin
4
  .DS_Store
5
  .venv
 
 
6
  *.mp4
7
  sandbox
database/completed/README.md DELETED
File without changes
database/pending/README.md DELETED
@@ -1 +0,0 @@
1
- Completed tasks go here
 
 
package.json CHANGED
@@ -7,7 +7,7 @@
7
  "start": "node --loader ts-node/esm src/index.mts",
8
  "test:submitVideo": "node --loader ts-node/esm src/tests/submitVideo.mts",
9
  "test:checkStatus": "node --loader ts-node/esm src/tests/checkStatus.mts",
10
- "test:downloadVideo": "node --loader ts-node/esm src/tests/downloadVideo.mts",
11
  "test:stuff": "node --loader ts-node/esm src/stuff.mts",
12
  "docker": "npm run docker:build && npm run docker:run",
13
  "docker:build": "docker build -t videochain-api .",
 
7
  "start": "node --loader ts-node/esm src/index.mts",
8
  "test:submitVideo": "node --loader ts-node/esm src/tests/submitVideo.mts",
9
  "test:checkStatus": "node --loader ts-node/esm src/tests/checkStatus.mts",
10
+ "test:downloadFileToTmp": "node --loader ts-node/esm src/tests/downloadFileToTmp.mts",
11
  "test:stuff": "node --loader ts-node/esm src/stuff.mts",
12
  "docker": "npm run docker:build && npm run docker:run",
13
  "docker:build": "docker build -t videochain-api .",
src/config.mts CHANGED
@@ -6,9 +6,9 @@ export const tasksDirPath = path.join(storagePath, "tasks")
6
  export const pendingTasksDirFilePath = path.join(tasksDirPath, "pending")
7
  export const completedTasksDirFilePath = path.join(tasksDirPath, "completed")
8
 
9
- export const videosDirPath = path.join(storagePath, "videos")
10
- export const pendingVideosDirFilePath = path.join(videosDirPath, "pending")
11
- export const completedVideosDirFilePath = path.join(videosDirPath, "completed")
12
 
13
  export const shotFormatVersion = 1
14
  export const sequenceFormatVersion = 1
 
6
  export const pendingTasksDirFilePath = path.join(tasksDirPath, "pending")
7
  export const completedTasksDirFilePath = path.join(tasksDirPath, "completed")
8
 
9
+ export const filesDirPath = path.join(storagePath, "files")
10
+ export const pendingFilesDirFilePath = path.join(filesDirPath, "pending")
11
+ export const completedFilesDirFilePath = path.join(filesDirPath, "completed")
12
 
13
  export const shotFormatVersion = 1
14
  export const sequenceFormatVersion = 1
src/index.mts CHANGED
@@ -1,12 +1,15 @@
1
- import { createReadStream, promises as fs } from "fs"
 
2
 
3
  import express from "express"
4
 
5
  import { VideoTask, VideoSequenceRequest } from "./types.mts"
6
  import { parseVideoRequest } from "./utils/parseVideoRequest.mts"
7
- import { savePendingTask } from "./database/savePendingTask.mts"
8
- import { getTask } from "./database/getTask.mts"
9
  import { main } from "./main.mts"
 
 
10
 
11
  main()
12
 
@@ -57,8 +60,6 @@ app.post("/", async (req, res) => {
57
  app.get("/:id", async (req, res) => {
58
  try {
59
  const task = await getTask(req.params.id)
60
- delete task.finalFilePath
61
- delete task.tmpFilePath
62
  res.status(200)
63
  res.write(JSON.stringify(task))
64
  res.end()
@@ -70,6 +71,30 @@ app.get("/:id", async (req, res) => {
70
  }
71
  })
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  app.get("/video/:id\.mp4", async (req, res) => {
74
  if (!req.params.id) {
75
  res.status(400)
@@ -89,8 +114,11 @@ app.get("/video/:id\.mp4", async (req, res) => {
89
  return
90
  }
91
 
 
92
 
93
- const filePath = task.finalFilePath || task.tmpFilePath || ''
 
 
94
  if (!filePath) {
95
  res.status(400)
96
  res.write(JSON.stringify({ error: "video exists, but cannot be previewed yet" }))
 
1
+ import { createReadStream, existsSync } from "node:fs"
2
+ import path from "node:path"
3
 
4
  import express from "express"
5
 
6
  import { VideoTask, VideoSequenceRequest } from "./types.mts"
7
  import { parseVideoRequest } from "./utils/parseVideoRequest.mts"
8
+ import { savePendingTask } from "./scheduler/savePendingTask.mts"
9
+ import { getTask } from "./scheduler/getTask.mts"
10
  import { main } from "./main.mts"
11
+ import { completedFilesDirFilePath } from "./config.mts"
12
+ import { deleteTask } from "./scheduler/deleteTask.mts"
13
 
14
  main()
15
 
 
60
  app.get("/:id", async (req, res) => {
61
  try {
62
  const task = await getTask(req.params.id)
 
 
63
  res.status(200)
64
  res.write(JSON.stringify(task))
65
  res.end()
 
71
  }
72
  })
73
 
74
+ app.delete("/:id", async (req, res) => {
75
+ let task: VideoTask = null
76
+ try {
77
+ task = await getTask(req.params.id)
78
+ } catch (err) {
79
+ console.error(err)
80
+ res.status(404)
81
+ res.write(JSON.stringify({ error: "couldn't find this task" }))
82
+ res.end()
83
+ }
84
+
85
+ try {
86
+ await deleteTask(task)
87
+ res.status(200)
88
+ res.write(JSON.stringify({ success: true }))
89
+ res.end()
90
+ } catch (err) {
91
+ console.error(err)
92
+ res.status(500)
93
+ res.write(JSON.stringify({ success: false, error: "failed to delete the task" }))
94
+ res.end()
95
+ }
96
+ })
97
+
98
  app.get("/video/:id\.mp4", async (req, res) => {
99
  if (!req.params.id) {
100
  res.status(400)
 
114
  return
115
  }
116
 
117
+ const completedFilePath = path.join(completedFilesDirFilePath, task.fileName)
118
 
119
+ // note: we DON'T want to use the pending file path, as there may be operations on it
120
+ // (ie. a process might be busy writing stuff to it)
121
+ const filePath = existsSync(completedFilePath) ? completedFilePath : ""
122
  if (!filePath) {
123
  res.status(400)
124
  res.write(JSON.stringify({ error: "video exists, but cannot be previewed yet" }))
src/initFolders.mts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { tasksDirPath, pendingTasksDirFilePath, completedTasksDirFilePath, filesDirPath, pendingFilesDirFilePath, completedFilesDirFilePath } from "./config.mts"
2
+ import { createDirIfNeeded } from "./utils/createDirIfNeeded.mts"
3
+
4
+ export const initFolders = () => {
5
+ console.log(`initializing folders..`)
6
+ createDirIfNeeded(tasksDirPath)
7
+ createDirIfNeeded(pendingTasksDirFilePath)
8
+ createDirIfNeeded(completedTasksDirFilePath)
9
+ createDirIfNeeded(filesDirPath)
10
+ createDirIfNeeded(pendingFilesDirFilePath)
11
+ createDirIfNeeded(completedFilesDirFilePath)
12
+ }
src/main.mts CHANGED
@@ -1,7 +1,11 @@
1
- import { getPendingTasks } from "./database/getPendingTasks.mts"
2
- import { processTask } from "./services/processTask.mts"
 
 
 
3
 
4
  export const main = async () => {
 
5
  const tasks = await getPendingTasks()
6
  if (!tasks.length) {
7
  setTimeout(() => {
 
1
+ import { initFolders } from "./initFolders.mts"
2
+ import { getPendingTasks } from "./scheduler/getPendingTasks.mts"
3
+ import { processTask } from "./scheduler/processTask.mts"
4
+
5
+ initFolders()
6
 
7
  export const main = async () => {
8
+
9
  const tasks = await getPendingTasks()
10
  if (!tasks.length) {
11
  setTimeout(() => {
src/{services β†’ production}/addAudioToVideo.mts RENAMED
@@ -5,6 +5,8 @@ import tmpDir from "temp-dir"
5
  import { v4 as uuidv4 } from "uuid"
6
 
7
  import ffmpeg from "fluent-ffmpeg"
 
 
8
 
9
  export const addAudioToVideo = async (
10
  videoFileName: string,
@@ -17,14 +19,12 @@ export const addAudioToVideo = async (
17
  * 2.0: amplify the audio to 200% of original volume (double volume - might cause clipping)
18
  */
19
  volume: number = 1.0
20
- ): Promise<string> => {
21
-
22
- const tempOutputFilePath = `${uuidv4()}.mp4`
23
- const videoFilePath = path.resolve(tmpDir, videoFileName)
24
  const audioFilePath = path.resolve(tmpDir, audioFileName)
25
 
26
  await new Promise((resolve, reject) => {
27
- ffmpeg(videoFilePath)
28
  .input(audioFilePath)
29
  .audioFilters({ filter: 'volume', options: volume }) // add audio filter for volume
30
  .outputOptions("-c:v copy") // use video copy codec
@@ -39,7 +39,5 @@ export const addAudioToVideo = async (
39
  })
40
 
41
  // Now we want to replace the original video file with the new file that has been created
42
- await fs.rename(tempOutputFilePath, videoFilePath)
43
-
44
- return videoFileName
45
  };
 
5
  import { v4 as uuidv4 } from "uuid"
6
 
7
  import ffmpeg from "fluent-ffmpeg"
8
+ import { pendingFilesDirFilePath } from "../config.mts"
9
+ import { moveFile } from "../utils/moveFile.mts"
10
 
11
  export const addAudioToVideo = async (
12
  videoFileName: string,
 
19
  * 2.0: amplify the audio to 200% of original volume (double volume - might cause clipping)
20
  */
21
  volume: number = 1.0
22
+ ) => {
23
+ const tempOutputFilePath = path.join(tmpDir, `${uuidv4()}.mp4`)
 
 
24
  const audioFilePath = path.resolve(tmpDir, audioFileName)
25
 
26
  await new Promise((resolve, reject) => {
27
+ ffmpeg(videoFileName)
28
  .input(audioFilePath)
29
  .audioFilters({ filter: 'volume', options: volume }) // add audio filter for volume
30
  .outputOptions("-c:v copy") // use video copy codec
 
39
  })
40
 
41
  // Now we want to replace the original video file with the new file that has been created
42
+ await moveFile(tempOutputFilePath, videoFileName)
 
 
43
  };
src/production/assembleShots.mts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+
3
+ import concat from 'ffmpeg-concat'
4
+
5
+ import { VideoShot } from '../types.mts'
6
+ import { pendingFilesDirFilePath } from "../config.mts"
7
+
8
+ export const assembleShots = async (shots: VideoShot[], fileName: string) => {
9
+
10
+ if (!Array.isArray(shots) || shots.length < 2) {
11
+ throw new Error(`need at least 2 shots`)
12
+ }
13
+
14
+ const transitions = [
15
+ {
16
+ name: 'circleOpen',
17
+ duration: 1000,
18
+ },
19
+ {
20
+ name: 'crossWarp',
21
+ duration: 800,
22
+ },
23
+ {
24
+ name: 'directionalWarp',
25
+ duration: 800,
26
+ // pass custom params to a transition
27
+ params: { direction: [1, -1] },
28
+ },
29
+ /*
30
+ {
31
+ name: 'squaresWire',
32
+ duration: 2000,
33
+ },
34
+ */
35
+ ]
36
+
37
+ const videoFilePath = path.join(pendingFilesDirFilePath, fileName)
38
+
39
+ const shotFilesPaths = shots.map(shot => path.join(
40
+ pendingFilesDirFilePath,
41
+ shot.fileName
42
+ ))
43
+
44
+ await concat({
45
+ output: videoFilePath,
46
+ videos: shotFilesPaths,
47
+ transitions: shotFilesPaths
48
+ .slice(0, shotFilesPaths.length - 1)
49
+ .map(
50
+ (vid) => transitions[Math.floor(Math.random() * transitions.length)]
51
+ ),
52
+ })
53
+ }
src/{services β†’ production}/generateActor.mts RENAMED
File without changes
src/{services β†’ production}/generateAudio.mts RENAMED
@@ -1,5 +1,11 @@
 
 
 
 
1
  import puppeteer from "puppeteer"
2
- import { downloadVideo } from "./downloadVideo.mts"
 
 
3
 
4
  const instances: string[] = [
5
  process.env.VS_AUDIO_GENERATION_SPACE_API_URL
@@ -42,15 +48,11 @@ export async function generateAudio(prompt: string, audioFileName: string) {
42
  const audioRemoteUrl = await page.$$eval("a[download]", el => el.map(x => x.getAttribute("href"))[0])
43
 
44
 
45
- console.log({
46
- audioRemoteUrl,
47
- })
48
-
49
-
50
- // console.log("downloading file from space..")
51
- console.log(`- downloading ${audioFileName} from ${audioRemoteUrl}`)
52
-
53
- await downloadVideo(audioRemoteUrl, audioFileName)
54
 
55
- return audioFileName
 
56
  }
 
1
+ import path from "node:path"
2
+
3
+ import { v4 as uuidv4 } from "uuid"
4
+ import tmpDir from "temp-dir"
5
  import puppeteer from "puppeteer"
6
+
7
+ import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
8
+ import { moveFileFromTmpToPending } from "../utils/moveFileFromTmpToPending.mts"
9
 
10
  const instances: string[] = [
11
  process.env.VS_AUDIO_GENERATION_SPACE_API_URL
 
48
  const audioRemoteUrl = await page.$$eval("a[download]", el => el.map(x => x.getAttribute("href"))[0])
49
 
50
 
51
+ // it is always a good idea to download to a tmp dir before saving to the pending dir
52
+ // because there is always a risk that the download will fail
53
+
54
+ const tmpFileName = `${uuidv4()}.mp4`
 
 
 
 
 
55
 
56
+ await downloadFileToTmp(audioRemoteUrl, tmpFileName)
57
+ await moveFileFromTmpToPending(tmpFileName, audioFileName)
58
  }
src/{services β†’ production}/generateAudioLegacy.mts RENAMED
@@ -1,6 +1,6 @@
1
  import { client } from '@gradio/client'
2
 
3
- import { generateSeed } from "./generateSeed.mts"
4
 
5
  const instances: string[] = [
6
  process.env.VS_AUDIO_GENERATION_SPACE_API_URL
 
1
  import { client } from '@gradio/client'
2
 
3
+ import { generateSeed } from "../utils/generateSeed.mts"
4
 
5
  const instances: string[] = [
6
  process.env.VS_AUDIO_GENERATION_SPACE_API_URL
src/{services β†’ production}/generateShot.mts RENAMED
@@ -3,12 +3,12 @@ import path from "node:path"
3
  import { v4 as uuidv4 } from "uuid"
4
  import tmpDir from "temp-dir"
5
 
6
- import { downloadVideo } from "./downloadVideo.mts"
7
  import { generateAudio } from "./generateAudio.mts"
8
  import { generateVideo } from "./generateVideo.mts"
9
  import { upscaleVideo } from "./upscaleVideo.mts"
10
  import { generateVoice } from "./generateVoice.mts"
11
- import { generateSeed } from "./generateSeed.mts"
12
  import { mergeAudio } from "./mergeAudio.mts"
13
  import { addAudioToVideo } from "./addAudioToVideo.mts"
14
  import { interpolateVideo } from "./interpolateVideo.mts"
@@ -106,7 +106,7 @@ export const generateShot = async ({
106
 
107
  console.log("downloading video..")
108
 
109
- const videoFileName = await downloadVideo(generatedVideoUrl, shotFileName)
110
 
111
  if (upscale) {
112
  console.log("upscaling video..")
@@ -135,7 +135,7 @@ export const generateShot = async ({
135
  const interpolationSteps = 3
136
  const interpolatedFramesPerSecond = 24
137
  await interpolateVideo(
138
- videoFileName,
139
  interpolationSteps,
140
  interpolatedFramesPerSecond
141
  )
@@ -194,7 +194,7 @@ export const generateShot = async ({
194
  audioFileName = foregroundAudioFileName
195
  }
196
 
197
- await addAudioToVideo(videoFileName, audioFileName)
198
  }
199
 
200
  console.log("returning result to user..")
 
3
  import { v4 as uuidv4 } from "uuid"
4
  import tmpDir from "temp-dir"
5
 
6
+ import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
7
  import { generateAudio } from "./generateAudio.mts"
8
  import { generateVideo } from "./generateVideo.mts"
9
  import { upscaleVideo } from "./upscaleVideo.mts"
10
  import { generateVoice } from "./generateVoice.mts"
11
+ import { generateSeed } from "../utils/generateSeed.mts"
12
  import { mergeAudio } from "./mergeAudio.mts"
13
  import { addAudioToVideo } from "./addAudioToVideo.mts"
14
  import { interpolateVideo } from "./interpolateVideo.mts"
 
106
 
107
  console.log("downloading video..")
108
 
109
+ const videoFileName = await downloadFileToTmp(generatedVideoUrl, shotFileName)
110
 
111
  if (upscale) {
112
  console.log("upscaling video..")
 
135
  const interpolationSteps = 3
136
  const interpolatedFramesPerSecond = 24
137
  await interpolateVideo(
138
+ task,
139
  interpolationSteps,
140
  interpolatedFramesPerSecond
141
  )
 
194
  audioFileName = foregroundAudioFileName
195
  }
196
 
197
+ await addAudioToVideo(task, audioFileName)
198
  }
199
 
200
  console.log("returning result to user..")
src/{services β†’ production}/generateVideo.mts RENAMED
@@ -1,7 +1,7 @@
1
  import { client } from "@gradio/client"
2
 
3
 
4
- import { generateSeed } from "./generateSeed.mts"
5
 
6
  const instances: string[] = [
7
  process.env.VS_VIDEO_GENERATION_SPACE_API_URL
 
1
  import { client } from "@gradio/client"
2
 
3
 
4
+ import { generateSeed } from "../utils/generateSeed.mts"
5
 
6
  const instances: string[] = [
7
  process.env.VS_VIDEO_GENERATION_SPACE_API_URL
src/{services β†’ production}/generateVoice.mts RENAMED
@@ -1,6 +1,6 @@
1
  import puppeteer from "puppeteer"
2
 
3
- import { downloadVideo } from "./downloadVideo.mts"
4
 
5
  const instances: string[] = [
6
  process.env.VS_VOICE_GENERATION_SPACE_API_URL
@@ -50,7 +50,7 @@ export async function generateVoice(prompt: string, voiceFileName: string) {
50
 
51
  console.log(`- downloading ${voiceFileName} from ${voiceRemoteUrl}`)
52
 
53
- await downloadVideo(voiceRemoteUrl, voiceFileName)
54
 
55
  return voiceFileName
56
  }
 
1
  import puppeteer from "puppeteer"
2
 
3
+ import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
4
 
5
  const instances: string[] = [
6
  process.env.VS_VOICE_GENERATION_SPACE_API_URL
 
50
 
51
  console.log(`- downloading ${voiceFileName} from ${voiceRemoteUrl}`)
52
 
53
+ await downloadFileToTmp(voiceRemoteUrl, voiceFileName)
54
 
55
  return voiceFileName
56
  }
src/{services β†’ production}/interpolateVideo.mts RENAMED
@@ -1,17 +1,20 @@
1
  import path from "node:path"
2
 
3
- import puppeteer from "puppeteer"
4
  import tmpDir from "temp-dir"
5
- import { downloadVideo } from "./downloadVideo.mts"
 
 
 
 
6
 
7
  const instances: string[] = [
8
  process.env.VS_VIDEO_INTERPOLATION_SPACE_API_URL
9
  ]
10
 
11
-
12
  // TODO we should use an inference endpoint instead
13
  export async function interpolateVideo(fileName: string, steps: number, fps: number) {
14
- const inputFilePath = path.join(tmpDir, fileName)
15
 
16
  console.log(`interpolating ${fileName}`)
17
  console.log(`warning: interpolateVideo parameter "${steps}" is ignored!`)
@@ -47,7 +50,11 @@ export async function interpolateVideo(fileName: string, steps: number, fps: num
47
 
48
  const interpolatedFileUrl = await page.$$eval('a[download="interpolated_result.mp4"]', el => el.map(x => x.getAttribute("href"))[0])
49
 
50
- await downloadVideo(interpolatedFileUrl, fileName)
 
 
 
51
 
52
- return fileName
 
53
  }
 
1
  import path from "node:path"
2
 
3
+ import { v4 as uuidv4 } from "uuid"
4
  import tmpDir from "temp-dir"
5
+ import puppeteer from "puppeteer"
6
+
7
+ import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
8
+ import { pendingFilesDirFilePath } from "../config.mts"
9
+ import { moveFileFromTmpToPending } from "../utils/moveFileFromTmpToPending.mts"
10
 
11
  const instances: string[] = [
12
  process.env.VS_VIDEO_INTERPOLATION_SPACE_API_URL
13
  ]
14
 
 
15
  // TODO we should use an inference endpoint instead
16
  export async function interpolateVideo(fileName: string, steps: number, fps: number) {
17
+ const inputFilePath = path.join(pendingFilesDirFilePath, fileName)
18
 
19
  console.log(`interpolating ${fileName}`)
20
  console.log(`warning: interpolateVideo parameter "${steps}" is ignored!`)
 
50
 
51
  const interpolatedFileUrl = await page.$$eval('a[download="interpolated_result.mp4"]', el => el.map(x => x.getAttribute("href"))[0])
52
 
53
+ // it is always a good idea to download to a tmp dir before saving to the pending dir
54
+ // because there is always a risk that the download will fail
55
+
56
+ const tmpFileName = `${uuidv4()}.mp4`
57
 
58
+ await downloadFileToTmp(interpolatedFileUrl, tmpFileName)
59
+ await moveFileFromTmpToPending(tmpFileName, fileName)
60
  }
src/{services β†’ production}/interpolateVideoLegacy.mts RENAMED
@@ -5,7 +5,7 @@ import { Blob } from "buffer"
5
  import { client } from "@gradio/client"
6
  import tmpDir from "temp-dir"
7
 
8
- import { downloadVideo } from './downloadVideo.mts'
9
 
10
  const instances: string[] = [
11
  process.env.VS_VIDEO_INTERPOLATION_SPACE_API_URL
@@ -35,5 +35,5 @@ export const interpolateVideo = async (fileName: string, steps: number, fps: num
35
  const { orig_name, data: remoteFilePath } = data
36
  const remoteUrl = `${instance}/file=${remoteFilePath}`
37
  console.log("remoteUrl:", remoteUrl)
38
- await downloadVideo(remoteUrl, fileName)
39
  }
 
5
  import { client } from "@gradio/client"
6
  import tmpDir from "temp-dir"
7
 
8
+ import { downloadFileToTmp } from '../utils/downloadFileToTmp.mts'
9
 
10
  const instances: string[] = [
11
  process.env.VS_VIDEO_INTERPOLATION_SPACE_API_URL
 
35
  const { orig_name, data: remoteFilePath } = data
36
  const remoteUrl = `${instance}/file=${remoteFilePath}`
37
  console.log("remoteUrl:", remoteUrl)
38
+ await downloadFileToTmp(remoteUrl, fileName)
39
  }
src/{services β†’ production}/mergeAudio.mts RENAMED
File without changes
src/{services β†’ production}/postInterpolation.mts RENAMED
@@ -1,11 +1,11 @@
1
  import path from "node:path"
2
- import fs from "node:fs"
3
 
4
  import { v4 as uuidv4 } from "uuid"
5
  import tmpDir from "temp-dir"
6
  import ffmpeg from "fluent-ffmpeg"
 
7
 
8
- export const postInterpolation = async (fileName: string, duration: number, nbFrames: number): Promise<string> => {
9
  return new Promise((resolve,reject) => {
10
 
11
  const tmpFileName = `${uuidv4()}.mp4`
@@ -13,15 +13,20 @@ export const postInterpolation = async (fileName: string, duration: number, nbFr
13
  const filePath = path.join(tmpDir, fileName)
14
  const tmpFilePath = path.join(tmpDir, tmpFileName)
15
 
16
-
17
  ffmpeg.ffprobe(filePath, function(err, metadata) {
18
  if (err) { reject(err); return; }
19
 
 
20
 
21
- const currentVideoDuration = metadata.format.duration
22
-
 
 
 
 
23
  // compute a ratio ex. 0.3 = 30% of the total length
24
- const durationRatio = currentVideoDuration / duration
 
25
 
26
  ffmpeg(filePath)
27
 
@@ -40,12 +45,7 @@ export const postInterpolation = async (fileName: string, duration: number, nbFr
40
 
41
  .save(tmpFilePath)
42
  .on("end", async () => {
43
- await fs.promises.copyFile(tmpFilePath, filePath)
44
- try {
45
- await fs.promises.unlink(tmpFilePath)
46
- } catch (err) {
47
- console.log("failed to cleanup (no big deal..)")
48
- }
49
 
50
  resolve(fileName)
51
  })
 
1
  import path from "node:path"
 
2
 
3
  import { v4 as uuidv4 } from "uuid"
4
  import tmpDir from "temp-dir"
5
  import ffmpeg from "fluent-ffmpeg"
6
+ import { moveFileFromTmpToPending } from "../utils/moveFileFromTmpToPending.mts"
7
 
8
+ export const postInterpolation = async (fileName: string, durationMs: number, nbFrames: number): Promise<string> => {
9
  return new Promise((resolve,reject) => {
10
 
11
  const tmpFileName = `${uuidv4()}.mp4`
 
13
  const filePath = path.join(tmpDir, fileName)
14
  const tmpFilePath = path.join(tmpDir, tmpFileName)
15
 
 
16
  ffmpeg.ffprobe(filePath, function(err, metadata) {
17
  if (err) { reject(err); return; }
18
 
19
+ const durationInSec = durationMs / 1000
20
 
21
+ const currentVideoDurationInSec = metadata.format.duration
22
+
23
+ console.log(`target duration in sec: ${currentVideoDurationInSec}s`)
24
+
25
+ console.log(`target duration in sec: ${durationInSec}s (${durationMs}ms)`)
26
+
27
  // compute a ratio ex. 0.3 = 30% of the total length
28
+ const durationRatio = currentVideoDurationInSec / durationInSec
29
+ console.log(`durationRatio: ${durationRatio} (${Math.round(durationRatio % 100)}%)`)
30
 
31
  ffmpeg(filePath)
32
 
 
45
 
46
  .save(tmpFilePath)
47
  .on("end", async () => {
48
+ await moveFileFromTmpToPending(tmpFileName, fileName)
 
 
 
 
 
49
 
50
  resolve(fileName)
51
  })
src/{services β†’ production}/upscaleVideo.mts RENAMED
@@ -1,9 +1,12 @@
1
- import path from 'node:path'
2
- import fs from 'node:fs'
3
 
4
- import tmpDir from 'temp-dir'
5
- import puppeteer from 'puppeteer'
6
- import { downloadVideo } from './downloadVideo.mts'
 
 
 
 
7
 
8
  const instances: string[] = [
9
  process.env.VS_VIDEO_UPSCALE_SPACE_API_URL
@@ -28,7 +31,7 @@ export async function upscaleVideo(fileName: string, prompt: string) {
28
  const promptField = await page.$('textarea')
29
  await promptField.type(prompt)
30
 
31
- const inputFilePath = path.join(tmpDir, fileName)
32
  // console.log(`local file to upscale: ${inputFilePath}`)
33
 
34
  await new Promise(r => setTimeout(r, 3000))
@@ -59,24 +62,11 @@ export async function upscaleVideo(fileName: string, prompt: string) {
59
 
60
  const upscaledFileUrl = await page.$$eval('a[download="xl_result.mp4"]', el => el.map(x => x.getAttribute("href"))[0])
61
 
62
- // console.log('downloading upscaled image from:', upscaledFileUrl)
63
-
64
- const tmpFileName = `${fileName}_xl`
65
-
66
- // console.log('downloading file from space..')
67
- console.log(`- downloading ${fileName} from ${upscaledFileUrl}`)
68
-
69
- await downloadVideo(upscaledFileUrl, tmpFileName)
70
-
71
- const tmpFilePath = path.join(tmpDir, tmpFileName)
72
- const filePath = path.join(tmpDir, fileName)
73
-
74
- await fs.promises.copyFile(tmpFilePath, filePath)
75
- try {
76
- await fs.promises.unlink(tmpFilePath)
77
- } catch (err) {
78
- console.log('failed to cleanup (no big deal..)')
79
- }
80
 
81
- return fileName
 
82
  }
 
1
+ import path from "node:path"
 
2
 
3
+ import { v4 as uuidv4 } from "uuid"
4
+ import tmpDir from "temp-dir"
5
+ import puppeteer from "puppeteer"
6
+
7
+ import { downloadFileToTmp } from '../utils/downloadFileToTmp.mts'
8
+ import { pendingFilesDirFilePath } from '../config.mts'
9
+ import { moveFileFromTmpToPending } from "../utils/moveFileFromTmpToPending.mts"
10
 
11
  const instances: string[] = [
12
  process.env.VS_VIDEO_UPSCALE_SPACE_API_URL
 
31
  const promptField = await page.$('textarea')
32
  await promptField.type(prompt)
33
 
34
+ const inputFilePath = path.join(pendingFilesDirFilePath, fileName)
35
  // console.log(`local file to upscale: ${inputFilePath}`)
36
 
37
  await new Promise(r => setTimeout(r, 3000))
 
62
 
63
  const upscaledFileUrl = await page.$$eval('a[download="xl_result.mp4"]', el => el.map(x => x.getAttribute("href"))[0])
64
 
65
+ // it is always a good idea to download to a tmp dir before saving to the pending dir
66
+ // because there is always a risk that the download will fail
67
+
68
+ const tmpFileName = `${uuidv4()}.mp4`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ await downloadFileToTmp(upscaledFileUrl, tmpFileName)
71
+ await moveFileFromTmpToPending(tmpFileName, fileName)
72
  }
src/scheduler/deleteTask.mts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { existsSync, promises as fs } from "node:fs"
3
+ import path from "node:path"
4
+
5
+ import tmpDir from "temp-dir"
6
+
7
+ import { VideoTask } from "../types.mts"
8
+ import { completedTasksDirFilePath, completedFilesDirFilePath, pendingTasksDirFilePath, pendingFilesDirFilePath } from "../config.mts"
9
+ import { deleteFileIfExists } from "../utils/deleteFileIfExists.mts"
10
+
11
+
12
+ export const deleteTask = async (task: VideoTask) => {
13
+ const taskFileName = `${task.id}.json`
14
+ const videoFileName = task.fileName
15
+
16
+ // .mp4 files
17
+ const tmpFilePath = path.join(tmpDir, videoFileName)
18
+ const pendingVideoPath = path.join(pendingFilesDirFilePath, videoFileName)
19
+ const completedVideoPath = path.join(completedFilesDirFilePath, videoFileName)
20
+
21
+ // .json files
22
+ const pendingTaskPath = path.join(pendingTasksDirFilePath, taskFileName)
23
+ const completedTaskPath = path.join(completedTasksDirFilePath, taskFileName)
24
+
25
+ await deleteFileIfExists(tmpFilePath)
26
+ await deleteFileIfExists(pendingVideoPath)
27
+ await deleteFileIfExists(completedVideoPath)
28
+ await deleteFileIfExists(pendingTaskPath)
29
+ await deleteFileIfExists(completedTaskPath)
30
+
31
+ // TODO: we didn't delete any audio file!
32
+ console.log(`note: we didn't delete any audio file!`)
33
+ }
src/{database β†’ scheduler}/getCompletedTasks.mts RENAMED
@@ -1,5 +1,5 @@
1
  import { VideoTask } from "../types.mts"
2
- import { completedTasksDirFilePath } from "./constants.mts"
3
  import { readTasks } from "./readTasks.mts"
4
 
5
  export const getCompletedTasks = async (): Promise<VideoTask[]> => {
 
1
  import { VideoTask } from "../types.mts"
2
+ import { completedTasksDirFilePath } from "../config.mts"
3
  import { readTasks } from "./readTasks.mts"
4
 
5
  export const getCompletedTasks = async (): Promise<VideoTask[]> => {
src/{database β†’ scheduler}/getPendingTasks.mts RENAMED
@@ -1,5 +1,5 @@
1
  import { VideoTask } from "../types.mts"
2
- import { pendingTasksDirFilePath } from "./constants.mts"
3
  import { readTasks } from "./readTasks.mts"
4
 
5
  export const getPendingTasks = async (): Promise<VideoTask[]> => {
 
1
  import { VideoTask } from "../types.mts"
2
+ import { pendingTasksDirFilePath } from "../config.mts"
3
  import { readTasks } from "./readTasks.mts"
4
 
5
  export const getPendingTasks = async (): Promise<VideoTask[]> => {
src/{database β†’ scheduler}/getTask.mts RENAMED
@@ -1,6 +1,6 @@
1
  import path from "node:path"
2
 
3
- import { completedTasksDirFilePath, pendingTasksDirFilePath } from "./constants.mts"
4
  import { readTask } from "./readTask.mts"
5
 
6
  export const getTask = async (id: string) => {
 
1
  import path from "node:path"
2
 
3
+ import { completedTasksDirFilePath, pendingTasksDirFilePath } from "../config.mts"
4
  import { readTask } from "./readTask.mts"
5
 
6
  export const getTask = async (id: string) => {
src/scheduler/processTask.mts ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { saveCompletedTask } from "./saveCompletedTask.mts"
2
+ import { savePendingTask } from "./savePendingTask.mts"
3
+ import { updatePendingTask } from "./updatePendingTask.mts"
4
+ import { VideoShot, VideoTask } from "../types.mts"
5
+ import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
6
+ import { generateVideo } from "../production/generateVideo.mts"
7
+ import { copyVideoFromTmpToPending } from "../utils/copyVideoFromTmpToPending.mts"
8
+ import { copyVideoFromTmpToCompleted } from "../utils/copyVideoFromTmpToCompleted.mts"
9
+ import { upscaleVideo } from "../production/upscaleVideo.mts"
10
+ import { interpolateVideo } from "../production/interpolateVideo.mts"
11
+ import { postInterpolation } from "../production/postInterpolation.mts"
12
+ import { moveVideoFromPendingToCompleted } from "../utils/moveVideoFromPendingToCompleted.mts"
13
+ import { assembleShots } from "../production/assembleShots.mts"
14
+
15
+ export const processTask = async (task: VideoTask) => {
16
+ console.log(`processing video task ${task.id}`)
17
+
18
+ // something isn't right, the task is already completed
19
+ if (task.completed) {
20
+ console.log(`video task ${task.id} is already completed`)
21
+ await saveCompletedTask(task)
22
+ return
23
+ }
24
+
25
+ for (const shot of task.shots) {
26
+ // skip shots completed previously
27
+ if (shot.completed) {
28
+ continue
29
+ }
30
+
31
+ console.log(`need to complete shot ${shot.id}`)
32
+
33
+
34
+ // currenty we cannot generate too many frames at once,
35
+ // otherwise the upscaler will have trouble
36
+
37
+ // so for now, we fix it to 24 frames
38
+ // const nbFramesForBaseModel = Math.min(3, Math.max(1, Math.round(duration))) * 8
39
+ const nbFramesForBaseModel = 24
40
+
41
+ if (!shot.hasGeneratedPreview) {
42
+ console.log("generating a preview of the final result..")
43
+ let generatedPreviewVideoUrl = ""
44
+ try {
45
+ generatedPreviewVideoUrl = await generateVideo(shot.shotPrompt, {
46
+ seed: shot.seed,
47
+ nbFrames: nbFramesForBaseModel,
48
+ nbSteps: 10, // for the preview, we only give a rough approximation
49
+ })
50
+
51
+ console.log("downloading preview video..")
52
+
53
+ // download to /tmp
54
+ await downloadFileToTmp(generatedPreviewVideoUrl, shot.fileName)
55
+
56
+ // NO NEED to copy from /tmp to /data/pending
57
+ // await copyVideoFromTmpToPending(shot.fileName)
58
+
59
+ // copy from /tmp to /data/completed
60
+ await copyVideoFromTmpToCompleted(shot.fileName)
61
+
62
+ shot.hasGeneratedPreview = true
63
+ shot.nbCompletedSteps++
64
+
65
+ await updatePendingTask(task)
66
+
67
+ } catch (err) {
68
+ console.error(`failed to generate preview for shot ${shot.id} (${err})`)
69
+ // something is wrong, let's put the whole thing back into the queue
70
+ task.error = `failed to generate preview for shot ${shot.id} (will try again later)`
71
+ await updatePendingTask(task)
72
+ break
73
+ }
74
+
75
+ }
76
+
77
+ if (!shot.hasGeneratedVideo) {
78
+ console.log("generating primordial pixel soup (raw video)..")
79
+ let generatedVideoUrl = ""
80
+
81
+
82
+ const nbFramesForBaseModel = 24
83
+
84
+ try {
85
+ generatedVideoUrl = await generateVideo(shot.shotPrompt, {
86
+ seed: shot.seed,
87
+ nbFrames: nbFramesForBaseModel,
88
+ nbSteps: shot.steps,
89
+ })
90
+
91
+ console.log("downloading video..")
92
+
93
+ await downloadFileToTmp(generatedVideoUrl, shot.fileName)
94
+
95
+ await copyVideoFromTmpToPending(shot.fileName)
96
+
97
+ shot.hasGeneratedVideo = true
98
+ shot.nbCompletedSteps++
99
+
100
+ await updatePendingTask(task)
101
+
102
+ } catch (err) {
103
+ console.error(`failed to generate shot ${shot.id} (${err})`)
104
+ // something is wrong, let's put the whole thing back into the queue
105
+ task.error = `failed to generate shot ${shot.id} (will try again later)`
106
+ await updatePendingTask(task)
107
+ break
108
+ }
109
+
110
+ }
111
+
112
+ if (!shot.hasUpscaledVideo) {
113
+ console.log("upscaling video..")
114
+ try {
115
+ await upscaleVideo(shot.fileName, shot.shotPrompt)
116
+
117
+ shot.hasUpscaledVideo = true
118
+ shot.nbCompletedSteps++
119
+
120
+ await updatePendingTask(task)
121
+ } catch (err) {
122
+ console.error(`failed to upscale shot ${shot.id} (${err})`)
123
+ // something is wrong, let's put the whole thing back into the queue
124
+ task.error = `failed to upscale shot ${shot.id} (will try again later)`
125
+ await updatePendingTask(task)
126
+ break
127
+ }
128
+ }
129
+
130
+ if (!shot.hasInterpolatedVideo) {
131
+ console.log("interpolating video..")
132
+ // ATTENTION 1:
133
+ // the interpolation step always create a SLOW MOTION video
134
+ // it means it can last a lot longer (eg. 2x, 3x, 4x.. longer)
135
+ // than the duration generated by the original video model
136
+
137
+ // ATTENTION 2:
138
+ // the interpolation step generates videos in 910x512!
139
+
140
+ // ATTENTION 3:
141
+ // the interpolation step parameters are currently not passed to the space,
142
+ // so changing those two variables below will have no effect!
143
+ const interpolationSteps = 3
144
+ const interpolatedFramesPerSecond = 24
145
+ console.log('creating slow-mo video (910x512 @ 24 FPS)')
146
+ try {
147
+ await interpolateVideo(
148
+ shot.fileName,
149
+ interpolationSteps,
150
+ interpolatedFramesPerSecond
151
+ )
152
+
153
+ shot.hasInterpolatedVideo = true
154
+ shot.nbCompletedSteps++
155
+
156
+ await updatePendingTask(task)
157
+
158
+ } catch (err) {
159
+ console.error(`failed to interpolate shot ${shot.id} (${err})`)
160
+ // something is wrong, let's put the whole thing back into the queue
161
+ task.error = `failed to interpolate shot ${shot.id} (will try again later)`
162
+ await updatePendingTask(task)
163
+ break
164
+ }
165
+ }
166
+
167
+
168
+ if (!shot.hasPostProcessedVideo) {
169
+ console.log("post-processing video..")
170
+
171
+ // with our current interpolation settings, the 3 seconds video generated by the model
172
+ // become a 7 seconds video, at 24 FPS
173
+
174
+ // so we want to scale it back to the desired duration length
175
+ // also, as a last trick we want to upscale it (without AI) and add some FXs
176
+ console.log('performing final scaling (1280x720 @ 24 FPS)')
177
+
178
+ try {
179
+ await postInterpolation(shot.fileName, shot.durationMs, shot.fps)
180
+
181
+ shot.hasPostProcessedVideo = true
182
+ shot.nbCompletedSteps++
183
+
184
+ await updatePendingTask(task)
185
+
186
+ } catch (err) {
187
+ console.error(`failed to post-process shot ${shot.id} (${err})`)
188
+ // something is wrong, let's put the whole thing back into the queue
189
+ task.error = `failed to post-process shot ${shot.id} (will try again later)`
190
+ await updatePendingTask(task)
191
+ break
192
+ }
193
+ }
194
+
195
+ shot.completed = true
196
+ shot.completedAt = new Date().toISOString()
197
+ task.nbCompletedShots++
198
+
199
+ await updatePendingTask(task)
200
+ }
201
+
202
+ console.log(`end of the loop:`)
203
+ console.log(`nb completed shots: ${task.nbCompletedShots}`)
204
+
205
+ if (task.nbCompletedShots === task.nbTotalShots) {
206
+ console.log(`we have completed the whole video sequence!`)
207
+ console.log(`assembling the video..`)
208
+
209
+ if (task.nbTotalShots === 1) {
210
+ console.log(`we only have one shot, so this gonna be easy`)
211
+ task.hasAssembledVideo = true
212
+
213
+ // the shot become the final movie
214
+ await moveVideoFromPendingToCompleted(task.shots[0].fileName, task.fileName)
215
+
216
+ await updatePendingTask(task)
217
+ }
218
+
219
+ if (!task.hasAssembledVideo) {
220
+ console.log(`assembling the ${task.shots.length} shots together (might take a while)`)
221
+ try {
222
+ await assembleShots(task.shots, task.fileName)
223
+ console.log(`finished assembling the ${task.shots.length} shots together!`)
224
+
225
+ await moveVideoFromPendingToCompleted(task.fileName)
226
+
227
+ task.hasAssembledVideo = true
228
+
229
+ await updatePendingTask(task)
230
+ } catch (err) {
231
+ console.error(`failed to assemble the shots together (${err})`)
232
+ // something is wrong, let's put the whole thing back into the queue
233
+ task.error = `failed to assemble the shots together (will try again later)`
234
+ await updatePendingTask(task)
235
+ return
236
+ }
237
+ }
238
+
239
+ task.completed = true
240
+ task.completedAt = new Date().toISOString()
241
+ await updatePendingTask(task)
242
+
243
+ console.log(`moving task to completed tasks..`)
244
+ await saveCompletedTask(task)
245
+ }
246
+ }
src/{database β†’ scheduler}/readTask.mts RENAMED
File without changes
src/{database β†’ scheduler}/readTasks.mts RENAMED
File without changes
src/{database β†’ scheduler}/saveCompletedTask.mts RENAMED
@@ -3,11 +3,11 @@ import path from "path"
3
 
4
  import { VideoTask } from "../types.mts"
5
  import { completedTasksDirFilePath, pendingTasksDirFilePath } from "../config.mts"
 
6
 
7
  export const saveCompletedTask = async (task: VideoTask) => {
8
  const fileName = `${task.id}.json`
9
  const pendingFilePath = path.join(pendingTasksDirFilePath, fileName)
10
  const completedFilePath = path.join(completedTasksDirFilePath, fileName)
11
- await fs.writeFile(completedFilePath, JSON.stringify(task, null, 2), "utf8")
12
- await fs.unlink(pendingFilePath)
13
  }
 
3
 
4
  import { VideoTask } from "../types.mts"
5
  import { completedTasksDirFilePath, pendingTasksDirFilePath } from "../config.mts"
6
+ import { moveFile } from "../utils/moveFile.mts"
7
 
8
  export const saveCompletedTask = async (task: VideoTask) => {
9
  const fileName = `${task.id}.json`
10
  const pendingFilePath = path.join(pendingTasksDirFilePath, fileName)
11
  const completedFilePath = path.join(completedTasksDirFilePath, fileName)
12
+ await moveFile(pendingFilePath, completedFilePath)
 
13
  }
src/{database β†’ scheduler}/savePendingTask.mts RENAMED
File without changes
src/{database β†’ scheduler}/updatePendingTask.mts RENAMED
File without changes
src/services/processTask.mts DELETED
@@ -1,68 +0,0 @@
1
- import { saveCompletedTask } from "../database/saveCompletedTask.mts";
2
- import { savePendingTask } from "../database/savePendingTask.mts";
3
- import { updatePendingTask } from "../database/updatePendingTask.mts";
4
- import { VideoTask } from "../types.mts";
5
- import { downloadVideo } from "./downloadVideo.mts";
6
- import { generateVideo } from "./generateVideo.mts";
7
-
8
- export const processTask = async (task: VideoTask) => {
9
- console.log(`processing video task ${task.id}`)
10
-
11
- // something isn't right, the task is already completed
12
- if (task.completed) {
13
- console.log(`video task ${task.id} is already completed`)
14
- await saveCompletedTask(task)
15
- return
16
- }
17
-
18
- let nbCompletedShots = 0
19
- for (const shot of task.shots) {
20
- // skip completed shots
21
- if (shot.completed) {
22
- nbCompletedShots++
23
- continue
24
- }
25
-
26
- console.log(`need to complete shot ${shot.id}`)
27
-
28
- const shotFileName = `${shot.id}.mp4`
29
-
30
- if (!shot.hasGeneratedVideo) {
31
- console.log("generating primordial pixel soup (raw video)..")
32
- let generatedVideoUrl = ""
33
-
34
- // currenty we cannot generate too many frames at once,
35
- // otherwise the upscaler will have trouble
36
-
37
- // so for now, we fix it to 24 frames
38
- // const nbFramesForBaseModel = Math.min(3, Math.max(1, Math.round(duration))) * 8
39
- const nbFramesForBaseModel = 24
40
-
41
- try {
42
- generatedVideoUrl = await generateVideo(shot.shotPrompt, {
43
- seed: shot.seed,
44
- nbFrames: nbFramesForBaseModel,
45
- nbSteps: shot.steps,
46
- })
47
-
48
- console.log("downloading video..")
49
-
50
- await downloadVideo(generatedVideoUrl, shotFileName)
51
-
52
- } catch (err) {
53
- // something is wrong, let's put the whole thing back into the queue
54
- task.error = `failed to generate shot ${shot.id} (will try again later)`
55
- await updatePendingTask(task)
56
- break
57
- }
58
-
59
-
60
- }
61
-
62
- if (!shot.hasUpscaledVideo) {
63
-
64
- }
65
-
66
- }
67
-
68
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/types.mts CHANGED
@@ -102,9 +102,6 @@ export type VideoTransition =
102
 
103
 
104
  export interface VideoShotMeta {
105
- // must be unique
106
- id: string
107
-
108
  shotPrompt: string
109
  // inputVideo?: string
110
 
@@ -135,24 +132,20 @@ export interface VideoShotMeta {
135
 
136
  introTransition: VideoTransition
137
  introDurationMs: number // in milliseconds
138
-
139
- // for internal use
140
- hasGeneratedVideo: boolean
141
- hasUpscaledVideo: boolean
142
- hasGeneratedBackgroundAudio: boolean
143
- hasGeneratedForegroundAudio: boolean
144
- hasGeneratedActor: boolean
145
- hasInterpolatedVideo: boolean
146
- hasAddedAudio: boolean
147
- hasPostProcessedVideo: boolean
148
  }
149
 
150
 
151
  export interface VideoShotData {
 
 
 
 
152
 
153
  // used to check compatibility
154
  version: number
155
 
 
 
156
  hasGeneratedVideo: boolean
157
  hasUpscaledVideo: boolean
158
  hasGeneratedBackgroundAudio: boolean
@@ -167,14 +160,11 @@ export interface VideoShotData {
167
  completedAt: string
168
  completed: boolean
169
  error: string
170
- filePath: string
171
  }
172
 
173
  export type VideoShot = VideoShotMeta & VideoShotData
174
 
175
  export interface VideoSequenceMeta {
176
- // must be unique
177
- id: string
178
 
179
  // describe the whole movie
180
  videoPrompt: string
@@ -198,7 +188,7 @@ export interface VideoSequenceMeta {
198
 
199
  noise: boolean // add movie noise
200
 
201
- steps: number
202
 
203
  fps: number // 8, 12, 24, 30, 60
204
 
@@ -210,17 +200,21 @@ export interface VideoSequenceMeta {
210
 
211
 
212
  export interface VideoSequenceData {
 
 
 
 
213
 
214
  // used to check compatibility
215
  version: number
216
 
 
217
  nbCompletedShots: number
218
  nbTotalShots: number
219
  progressPercent: number
220
  completedAt: string
221
  completed: boolean
222
  error: string
223
- filePath: string
224
  }
225
 
226
  export type VideoSequence = VideoSequenceMeta & VideoSequenceData
 
102
 
103
 
104
  export interface VideoShotMeta {
 
 
 
105
  shotPrompt: string
106
  // inputVideo?: string
107
 
 
132
 
133
  introTransition: VideoTransition
134
  introDurationMs: number // in milliseconds
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
 
138
  export interface VideoShotData {
139
+ // must be unique
140
+ id: string
141
+
142
+ fileName: string
143
 
144
  // used to check compatibility
145
  version: number
146
 
147
+ // for internal use
148
+ hasGeneratedPreview: boolean
149
  hasGeneratedVideo: boolean
150
  hasUpscaledVideo: boolean
151
  hasGeneratedBackgroundAudio: boolean
 
160
  completedAt: string
161
  completed: boolean
162
  error: string
 
163
  }
164
 
165
  export type VideoShot = VideoShotMeta & VideoShotData
166
 
167
  export interface VideoSequenceMeta {
 
 
168
 
169
  // describe the whole movie
170
  videoPrompt: string
 
188
 
189
  noise: boolean // add movie noise
190
 
191
+ steps: number // between 10 and 50
192
 
193
  fps: number // 8, 12, 24, 30, 60
194
 
 
200
 
201
 
202
  export interface VideoSequenceData {
203
+ // must be unique
204
+ id: string
205
+
206
+ fileName: string
207
 
208
  // used to check compatibility
209
  version: number
210
 
211
+ hasAssembledVideo: boolean
212
  nbCompletedShots: number
213
  nbTotalShots: number
214
  progressPercent: number
215
  completedAt: string
216
  completed: boolean
217
  error: string
 
218
  }
219
 
220
  export type VideoSequence = VideoSequenceMeta & VideoSequenceData
src/utils/copyVideoFromPendingToCompleted.mts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import { promises as fs } from "node:fs"
3
+
4
+ import { completedTasksDirFilePath, pendingFilesDirFilePath } from "../config.mts"
5
+
6
+ export const copyVideoFromPendingToCompleted = async (pendingFileName: string, completedFileName?: string) => {
7
+ if (!completedFileName) {
8
+ completedFileName = pendingFileName
9
+ }
10
+ const pendingFilePath = path.join(pendingFilesDirFilePath, pendingFileName)
11
+ const completedFilePath = path.join(completedTasksDirFilePath, completedFileName)
12
+
13
+ await fs.copyFile(pendingFilePath, completedFilePath)
14
+ console.log(`copied file from ${pendingFilePath} to ${completedFilePath}`)
15
+ }
src/utils/copyVideoFromTmpToCompleted.mts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import { promises as fs } from "node:fs"
3
+
4
+ import tmpDir from "temp-dir"
5
+ import { completedFilesDirFilePath } from "../config.mts"
6
+
7
+ // a function to copy a video to the completed video directory
8
+ // this implementation is safe to use on a Hugging Face Space
9
+ // for instance when copying from one disk to another
10
+ // (we cannot use fs.rename in that case)
11
+ export const copyVideoFromTmpToCompleted = async (tmpFileName: string, completedFileName?: string) => {
12
+ if (!completedFileName) {
13
+ completedFileName = tmpFileName
14
+ }
15
+ const tmpFilePath = path.join(tmpDir, tmpFileName)
16
+ const completedFilePath = path.join(completedFilesDirFilePath, completedFileName)
17
+
18
+ await fs.copyFile(tmpFilePath, completedFilePath)
19
+ console.log(`copied file from ${tmpFilePath} to ${completedFilePath}`)
20
+ }
src/utils/copyVideoFromTmpToPending.mts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import { promises as fs } from "node:fs"
3
+
4
+ import tmpDir from "temp-dir"
5
+ import { pendingFilesDirFilePath } from "../config.mts"
6
+ import { moveFile } from "./moveFile.mts"
7
+
8
+ // a function to copy a video to the pending video directory
9
+ // this implementation is safe to use on a Hugging Face Space
10
+ // for instance when copying from one disk to another
11
+ // (we cannot use fs.rename in that case)
12
+ export const copyVideoFromTmpToPending = async (tmpFileName: string, pendingFileName?: string) => {
13
+ if (!pendingFileName) {
14
+ pendingFileName = tmpFileName
15
+ }
16
+ const tmpFilePath = path.join(tmpDir, tmpFileName)
17
+ const pendingFilePath = path.join(pendingFilesDirFilePath, pendingFileName)
18
+
19
+ await fs.copyFile(tmpFilePath, pendingFilePath)
20
+ console.log(`copied file from ${tmpFilePath} to ${pendingFilePath}`)
21
+ }
src/utils/createDirIfNeeded.mts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { existsSync, mkdirSync } from "node:fs"
2
+
3
+ export const createDirIfNeeded = (dirPath: string) => {
4
+ if (!existsSync(dirPath)) {
5
+ mkdirSync(dirPath, { recursive: true })
6
+ }
7
+ }
src/utils/deleteFileIfExists.mts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { existsSync, promises as fs } from "node:fs"
2
+
3
+ export const deleteFileIfExists = async (filePath: string) => {
4
+ if (existsSync(filePath)) {
5
+ try {
6
+ await fs.unlink(filePath)
7
+ return true
8
+ } catch (err) {
9
+ console.log(`failed to delete file ${filePath}`)
10
+ }
11
+ }
12
+ return false
13
+ }
src/{services/downloadVideo.mts β†’ utils/downloadFileToTmp.mts} RENAMED
@@ -1,17 +1,18 @@
1
- import path from 'node:path'
2
- import fs from 'node:fs'
3
- import { pendingVideosDirFilePath } from '../config.mts'
4
 
5
- export const downloadVideo = async (remoteUrl: string, fileName: string): Promise<string> => {
6
 
7
- const filePath = path.resolve(pendingVideosDirFilePath, fileName)
 
 
8
 
9
  const controller = new AbortController()
10
  const timeoutId = setTimeout(() => controller.abort(), 15 * 60 * 60 * 1000) // 15 minutes
11
 
12
  // TODO finish the timeout?
13
 
14
- // download the video
15
  const response = await fetch(remoteUrl, {
16
  signal: controller.signal
17
  })
@@ -23,6 +24,4 @@ export const downloadVideo = async (remoteUrl: string, fileName: string): Promis
23
  filePath,
24
  Buffer.from(arrayBuffer)
25
  )
26
-
27
- return fileName
28
  }
 
1
+ import path from "node:path"
2
+ import fs from "node:fs"
 
3
 
4
+ import tmpDir from "temp-dir"
5
 
6
+ export const downloadFileToTmp = async (remoteUrl: string, fileName: string) => {
7
+
8
+ const filePath = path.resolve(tmpDir, fileName)
9
 
10
  const controller = new AbortController()
11
  const timeoutId = setTimeout(() => controller.abort(), 15 * 60 * 60 * 1000) // 15 minutes
12
 
13
  // TODO finish the timeout?
14
 
15
+ // download the file
16
  const response = await fetch(remoteUrl, {
17
  signal: controller.signal
18
  })
 
24
  filePath,
25
  Buffer.from(arrayBuffer)
26
  )
 
 
27
  }
src/{services β†’ utils}/generateSeed.mts RENAMED
File without changes
src/utils/moveFile.mts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from "node:fs"
2
+
3
+ // a function to move a file
4
+ // this implementation is safe to use on a Hugging Face Space
5
+ // for instance when copying from one disk to another
6
+ // (we cannot use fs.rename in that case)
7
+ export const moveFile = async (sourceFilePath: string, targetFilePath: string) => {
8
+ await fs.copyFile(sourceFilePath, targetFilePath)
9
+ console.log(`moved file from ${sourceFilePath} to ${targetFilePath}`)
10
+ try {
11
+ await fs.unlink(sourceFilePath)
12
+ } catch (err) {
13
+ console.log("moveFile: failed to cleanup (no big deal..)")
14
+ }
15
+ }
src/utils/moveFileFromTmpToPending.mts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import tmpDir from "temp-dir"
3
+ import { pendingFilesDirFilePath } from "../config.mts"
4
+ import { moveFile } from "./moveFile.mts"
5
+
6
+ // a function to move a file to the pending file directory
7
+ // this implementation is safe to use on a Hugging Face Space
8
+ // for instance when copying from one disk to another
9
+ // (we cannot use fs.rename in that case)
10
+ export const moveFileFromTmpToPending = async (tmpFileName: string, pendingFileName?: string) => {
11
+ if (!pendingFileName) {
12
+ pendingFileName = tmpFileName
13
+ }
14
+ const tmpFilePath = path.join(tmpDir, tmpFileName)
15
+ const pendingFilePath = path.join(pendingFilesDirFilePath, pendingFileName)
16
+
17
+ await moveFile(tmpFilePath, pendingFilePath)
18
+ }
src/utils/moveVideoFromPendingToCompleted.mts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "path"
2
+
3
+ import { completedFilesDirFilePath, pendingFilesDirFilePath } from "../config.mts"
4
+ import { moveFile } from "./moveFile.mts"
5
+
6
+ export const moveVideoFromPendingToCompleted = async (pendingFileName: string, completedFileName?: string) => {
7
+ if (!completedFileName) {
8
+ completedFileName = pendingFileName
9
+ }
10
+ const pendingFilePath = path.join(pendingFilesDirFilePath, pendingFileName)
11
+ const completedFilePath = path.join(completedFilesDirFilePath, completedFileName)
12
+
13
+ await moveFile(pendingFilePath, completedFilePath)
14
+ }
src/utils/moveVideoFromTmpToCompleted.mts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import tmpDir from "temp-dir"
3
+ import { completedFilesDirFilePath } from "../config.mts"
4
+ import { moveFile } from "./moveFile.mts"
5
+
6
+ // a function to move a video to the completed video directory
7
+ // this implementation is safe to use on a Hugging Face Space
8
+ // for instance when copying from one disk to another
9
+ // (we cannot use fs.rename in that case)
10
+ export const moveVideoFromTmpToCompleted = async (tmpFileName: string, completedFileName?: string) => {
11
+ if (!completedFileName) {
12
+ completedFileName = tmpFileName
13
+ }
14
+ const tmpFilePath = path.join(tmpDir, tmpFileName)
15
+ const completedFilePath = path.join(completedFilesDirFilePath, completedFileName)
16
+
17
+ await moveFile(tmpFilePath, completedFilePath)
18
+ }
src/utils/parseShotRequest.mts CHANGED
@@ -3,14 +3,17 @@ import { v4 as uuidv4 } from "uuid"
3
  // convert a request (which might be invalid)
4
 
5
  import { VideoSequence, VideoShot, VideoShotMeta } from "../types.mts"
6
- import { generateSeed } from "../services/generateSeed.mts"
7
  import { getValidNumber } from "./getValidNumber.mts"
8
- import { shotFormatVersion } from "../database/constants.mts"
9
 
10
  export const parseShotRequest = async (sequence: VideoSequence, maybeShotMeta: VideoShotMeta): Promise<VideoShot> => {
 
 
 
11
 
12
  const shot: VideoShot = {
13
- id: maybeShotMeta.id || uuidv4(),
14
 
15
  shotPrompt: `${maybeShotMeta.shotPrompt || ""}`,
16
 
@@ -39,7 +42,7 @@ export const parseShotRequest = async (sequence: VideoSequence, maybeShotMeta: V
39
  durationMs: getValidNumber(maybeShotMeta.durationMs, 0, 6000, 3000),
40
 
41
  // a video sequence CAN HAVE inconsistent iteration steps
42
- steps: getValidNumber(maybeShotMeta.steps || sequence.steps, 1, 60, 35),
43
 
44
  // a video sequence MUST HAVE consistent frames per second
45
  fps: getValidNumber(sequence.fps, 8, 60, 24),
@@ -54,6 +57,8 @@ export const parseShotRequest = async (sequence: VideoSequence, maybeShotMeta: V
54
  // for internal use
55
 
56
  version: shotFormatVersion,
 
 
57
  hasGeneratedVideo: false,
58
  hasUpscaledVideo: false,
59
  hasGeneratedBackgroundAudio: false,
@@ -77,7 +82,6 @@ export const parseShotRequest = async (sequence: VideoSequence, maybeShotMeta: V
77
  completedAt: '',
78
  completed: false,
79
  error: '',
80
- filePath: '',
81
  }
82
 
83
  return shot
 
3
  // convert a request (which might be invalid)
4
 
5
  import { VideoSequence, VideoShot, VideoShotMeta } from "../types.mts"
6
+ import { generateSeed } from "./generateSeed.mts"
7
  import { getValidNumber } from "./getValidNumber.mts"
8
+ import { shotFormatVersion } from "../config.mts"
9
 
10
  export const parseShotRequest = async (sequence: VideoSequence, maybeShotMeta: VideoShotMeta): Promise<VideoShot> => {
11
+ // we don't want people to input their own ID or we might have trouble,
12
+ // such as people attempting to use a non-UUID, a file path (to hack us), etc
13
+ const id = uuidv4()
14
 
15
  const shot: VideoShot = {
16
+ id,
17
 
18
  shotPrompt: `${maybeShotMeta.shotPrompt || ""}`,
19
 
 
42
  durationMs: getValidNumber(maybeShotMeta.durationMs, 0, 6000, 3000),
43
 
44
  // a video sequence CAN HAVE inconsistent iteration steps
45
+ steps: getValidNumber(maybeShotMeta.steps || sequence.steps, 10, 50, 35),
46
 
47
  // a video sequence MUST HAVE consistent frames per second
48
  fps: getValidNumber(sequence.fps, 8, 60, 24),
 
57
  // for internal use
58
 
59
  version: shotFormatVersion,
60
+ fileName: `${id}.mp4`,
61
+ hasGeneratedPreview: false,
62
  hasGeneratedVideo: false,
63
  hasUpscaledVideo: false,
64
  hasGeneratedBackgroundAudio: false,
 
82
  completedAt: '',
83
  completed: false,
84
  error: '',
 
85
  }
86
 
87
  return shot
src/utils/parseVideoRequest.mts CHANGED
@@ -3,18 +3,21 @@ import { v4 as uuidv4 } from "uuid"
3
  // convert a request (which might be invalid)
4
 
5
  import { VideoSequenceRequest, VideoTask } from "../types.mts"
6
- import { generateSeed } from "../services/generateSeed.mts"
7
  import { getValidNumber } from "./getValidNumber.mts"
8
  import { getValidResolution } from "./getValidResolution.mts"
9
  import { parseShotRequest } from "./parseShotRequest.mts"
10
- import { sequenceFormatVersion } from "../database/constants.mts"
 
11
 
12
 
13
  export const parseVideoRequest = async (request: VideoSequenceRequest): Promise<VideoTask> => {
 
 
 
14
 
15
  const task: VideoTask = {
16
  // ------------ VideoSequenceMeta -------------
17
- id: uuidv4(),
18
 
19
  // describe the whole movie
20
  videoPrompt: `${request.sequence.videoPrompt || ''}`,
@@ -38,7 +41,7 @@ export const parseVideoRequest = async (request: VideoSequenceRequest): Promise<
38
 
39
  noise: request.sequence.noise === true,
40
 
41
- steps: getValidNumber(request.sequence.steps, 1, 60, 35),
42
 
43
  fps: getValidNumber(request.sequence.fps, 8, 60, 24),
44
 
@@ -49,13 +52,15 @@ export const parseVideoRequest = async (request: VideoSequenceRequest): Promise<
49
 
50
  // ---------- VideoSequenceData ---------
51
  version: sequenceFormatVersion,
 
 
52
  nbCompletedShots: 0,
53
  nbTotalShots: 0,
54
  progressPercent: 0,
55
  completedAt: null,
56
  completed: false,
57
  error: '',
58
- filePath: '',
59
 
60
  // ------- the VideoShot -----
61
 
 
3
  // convert a request (which might be invalid)
4
 
5
  import { VideoSequenceRequest, VideoTask } from "../types.mts"
 
6
  import { getValidNumber } from "./getValidNumber.mts"
7
  import { getValidResolution } from "./getValidResolution.mts"
8
  import { parseShotRequest } from "./parseShotRequest.mts"
9
+ import { generateSeed } from "./generateSeed.mts"
10
+ import { sequenceFormatVersion } from "../config.mts"
11
 
12
 
13
  export const parseVideoRequest = async (request: VideoSequenceRequest): Promise<VideoTask> => {
14
+ // we don't want people to input their own ID or we might have trouble,
15
+ // such as people attempting to use a non-UUID, a file path (to hack us), etc
16
+ const id = uuidv4()
17
 
18
  const task: VideoTask = {
19
  // ------------ VideoSequenceMeta -------------
20
+ id,
21
 
22
  // describe the whole movie
23
  videoPrompt: `${request.sequence.videoPrompt || ''}`,
 
41
 
42
  noise: request.sequence.noise === true,
43
 
44
+ steps: getValidNumber(request.sequence.steps, 10, 50, 35),
45
 
46
  fps: getValidNumber(request.sequence.fps, 8, 60, 24),
47
 
 
52
 
53
  // ---------- VideoSequenceData ---------
54
  version: sequenceFormatVersion,
55
+ fileName: `${id}.mp4`,
56
+ hasAssembledVideo: false,
57
  nbCompletedShots: 0,
58
  nbTotalShots: 0,
59
  progressPercent: 0,
60
  completedAt: null,
61
  completed: false,
62
  error: '',
63
+
64
 
65
  // ------- the VideoShot -----
66