jbilcke-hf HF staff commited on
Commit
652f343
Β·
1 Parent(s): 3ecab68

working on the VideoChain queue system

Browse files
database/completed/README.md ADDED
File without changes
database/pending/README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ Completed tasks go here
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -5,8 +5,10 @@
5
  "main": "src/index.mts",
6
  "scripts": {
7
  "start": "node --loader ts-node/esm src/index.mts",
8
- "test": "node --loader ts-node/esm src/test.mts",
9
- "test2": "node --loader ts-node/esm src/test2.mts",
 
 
10
  "docker": "npm run docker:build && npm run docker:run",
11
  "docker:build": "docker build -t videochain-api .",
12
  "docker:run": "docker run -it -p 7860:7860 videochain-api"
@@ -17,8 +19,10 @@
17
  "@gradio/client": "^0.1.4",
18
  "@huggingface/inference": "^2.6.1",
19
  "@types/express": "^4.17.17",
 
20
  "@types/uuid": "^9.0.2",
21
  "express": "^4.18.2",
 
22
  "fluent-ffmpeg": "^2.1.2",
23
  "fs-extra": "^11.1.1",
24
  "node-fetch": "^3.3.1",
 
5
  "main": "src/index.mts",
6
  "scripts": {
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 .",
14
  "docker:run": "docker run -it -p 7860:7860 videochain-api"
 
19
  "@gradio/client": "^0.1.4",
20
  "@huggingface/inference": "^2.6.1",
21
  "@types/express": "^4.17.17",
22
+ "@types/ffmpeg-concat": "^1.1.2",
23
  "@types/uuid": "^9.0.2",
24
  "express": "^4.18.2",
25
+ "ffmpeg-concat": "^1.3.0",
26
  "fluent-ffmpeg": "^2.1.2",
27
  "fs-extra": "^11.1.1",
28
  "node-fetch": "^3.3.1",
src/database/constants.mts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+
2
+ export const pendingTasksDirFilePath = './database/pending/'
3
+ export const completedTasksDirFilePath = './database/completed/'
src/database/getCompletedTasks.mts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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[]> => {
6
+ const completedTasks = await readTasks(completedTasksDirFilePath)
7
+
8
+ return completedTasks
9
+ }
src/database/getPendingTasks.mts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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[]> => {
6
+ const pendingTasks = await readTasks(pendingTasksDirFilePath)
7
+
8
+ return pendingTasks
9
+ }
src/database/getTask.mts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) => {
7
+ const taskFileName = `${id}.json`
8
+
9
+ const completedTaskFilePath = path.join(completedTasksDirFilePath, taskFileName)
10
+ const pendingTaskFilePath = path.join(pendingTasksDirFilePath, taskFileName)
11
+
12
+ try {
13
+ const completedTask = await readTask(completedTaskFilePath)
14
+ return completedTask
15
+ } catch (err) {
16
+ try {
17
+ const pendingTask = await readTask(pendingTaskFilePath)
18
+ return pendingTask
19
+ } catch (err) {
20
+ throw new Error(`couldn't find task ${id}`)
21
+ }
22
+ }
23
+ }
src/database/readTask.mts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from "node:fs"
2
+ import path from "node:path"
3
+
4
+ import { VideoTask } from "../types.mts"
5
+
6
+ export const readTask = async (taskFilePath: string): Promise<VideoTask> => {
7
+ const task = JSON.parse(
8
+ await fs.readFile(taskFilePath, 'utf8')
9
+ ) as VideoTask
10
+
11
+ return task
12
+ }
src/database/readTasks.mts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import { promises as fs } from "node:fs"
3
+
4
+ import { VideoTask } from "../types.mts"
5
+ import { readTask } from "./readTask.mts"
6
+
7
+
8
+ export const readTasks = async (taskDirFilePath: string): Promise<VideoTask[]> => {
9
+
10
+ let tasksFiles: string[] = []
11
+ try {
12
+ const filesInDir = await fs.readdir(taskDirFilePath)
13
+
14
+ // we only keep valid files (in UUID.json format)
15
+ tasksFiles = filesInDir.filter(fileName => fileName.match(/[a-z0-9\-]\.json/i))
16
+ } catch (err) {
17
+ console.log(`failed to read tasks: ${err}`)
18
+ }
19
+
20
+ const tasks: VideoTask[] = []
21
+
22
+ for (const taskFileName in tasksFiles) {
23
+ const taskFilePath = path.join(taskDirFilePath, taskFileName)
24
+ try {
25
+ const task = await readTask(taskFilePath)
26
+ tasks.push(task)
27
+ } catch (parsingErr) {
28
+ console.log(`failed to read ${taskFileName}: ${parsingErr}`)
29
+ console.log(`deleting corrupted file ${taskFileName}`)
30
+ try {
31
+ await fs.unlink(taskFilePath)
32
+ } catch (unlinkErr) {
33
+ console.log(`failed to unlink ${taskFileName}: ${unlinkErr}`)
34
+ }
35
+ }
36
+ }
37
+
38
+ return tasks
39
+ }
src/database/saveCompletedTask.mts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from "node:fs"
2
+ import path from "path"
3
+
4
+ import { VideoTask } from "../types.mts"
5
+ import { completedTasksDirFilePath, pendingTasksDirFilePath } from "./constants.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
+ }
src/database/savePendingTask.mts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from "node:fs"
2
+ import path from "path"
3
+
4
+ import { VideoTask } from "../types.mts"
5
+ import { pendingTasksDirFilePath } from "./constants.mts"
6
+
7
+ export const savePendingTask = async (task: VideoTask) => {
8
+ const fileName = `${task.id}.json`
9
+ const filePath = path.join(pendingTasksDirFilePath, fileName)
10
+ await fs.writeFile(filePath, JSON.stringify(task, null, 2), "utf8")
11
+ }
src/index.mts CHANGED
@@ -1,112 +1,123 @@
1
- import { promises as fs } from "fs"
2
 
3
  import express from "express"
4
 
5
- import { generateSeed } from "./services/generateSeed.mts"
6
- import { Job, ShotQuery } from "./types.mts"
7
- import { generateShot } from "./services/generateShot.mts"
 
 
 
 
8
 
9
  const app = express()
10
  const port = 7860
11
 
12
  app.use(express.json())
13
 
14
- const queue: Job[] = []
15
-
16
- app.post("/shot", async (req, res) => {
17
- const query = req.body as ShotQuery
18
-
19
- const token = `${query.token || ""}`
20
  if (token !== process.env.VS_SECRET_ACCESS_TOKEN) {
21
  console.log("couldn't find access token in the query")
22
- res.write(JSON.stringify({ error: true, message: "access denied" }))
 
23
  res.end()
24
  return
25
  }
26
 
27
- const shotPrompt = `${query.shotPrompt || ""}`
28
- if (shotPrompt.length < 5) {
29
- res.write(JSON.stringify({ error: true, message: "prompt too short (must be at least 5 in length)" }))
 
 
 
 
 
 
30
  res.end()
31
  return
32
  }
33
 
34
- // optional video URL
35
- // const inputVideo = `${req.query.inputVideo || ""}`
36
-
37
- // optional background audio prompt
38
- const backgroundAudioPrompt = `${query.backgroundAudioPrompt || ""}`
 
 
 
 
 
 
 
 
39
 
40
- // optional foreground audio prompt
41
- const foregroundAudioPrompt = `${query.foregroundAudioPrompt || ""}`
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- // optional seed
44
- const defaultSeed = generateSeed()
45
- const seedStr = Number(`${query.seed || defaultSeed}`)
46
- const maybeSeed = Number(seedStr)
47
- const seed = isNaN(maybeSeed) || ! isFinite(maybeSeed) ? defaultSeed : maybeSeed
48
-
49
- // in production we want those ON by default
50
- const upscale = `${query.upscale || "true"}` === "true"
51
- const interpolate = `${query.upscale || "true"}` === "true"
52
- const noise = `${query.noise || "true"}` === "true"
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- const defaultDuration = 3
56
- const maxDuration = 5
57
- const durationStr = Number(`${query.duration || defaultDuration}`)
58
- const maybeDuration = Number(durationStr)
59
- const duration = Math.min(maxDuration, Math.max(1, isNaN(maybeDuration) || !isFinite(maybeDuration) ? defaultDuration : maybeDuration))
60
-
61
- const defaultSteps = 35
62
- const stepsStr = Number(`${query.steps || defaultSteps}`)
63
- const maybeSteps = Number(stepsStr)
64
- const nbSteps = Math.min(60, Math.max(1, isNaN(maybeSteps) || !isFinite(maybeSteps) ? defaultSteps : maybeSteps))
65
 
66
- // const frames per second
67
- const defaultFps = 24
68
- const fpsStr = Number(`${query.fps || defaultFps}`)
69
- const maybeFps = Number(fpsStr)
70
- const nbFrames = Math.min(60, Math.max(8, isNaN(maybeFps) || !isFinite(maybeFps) ? defaultFps : maybeFps))
71
-
72
- const defaultResolution = 576
73
- const resolutionStr = Number(`${query.resolution || defaultResolution}`)
74
- const maybeResolution = Number(resolutionStr)
75
- const resolution = Math.min(1080, Math.max(256, isNaN(maybeResolution) || !isFinite(maybeResolution) ? defaultResolution : maybeResolution))
76
-
77
- const actorPrompt = `${query.actorPrompt || ""}`
78
-
79
- const actorVoicePrompt = `${query.actorVoicePrompt || ""}`
80
-
81
- const actorDialoguePrompt = `${query.actorDialoguePrompt || ""}`
82
-
83
-
84
- const { filePath } = await generateShot({
85
- seed,
86
- actorPrompt,
87
- shotPrompt,
88
- backgroundAudioPrompt,
89
- foregroundAudioPrompt,
90
- actorDialoguePrompt,
91
- actorVoicePrompt,
92
- duration,
93
- nbFrames,
94
- resolution,
95
- nbSteps,
96
- upscale,
97
- interpolate,
98
- noise,
99
- })
100
-
101
- console.log(`generated video in ${filePath}`)
102
-
103
- console.log("returning result to user..")
104
-
105
- const buffer = await fs.readFile(filePath)
106
-
107
- res.setHeader("Content-Type", "media/mp4")
108
- res.setHeader("Content-Length", buffer.length)
109
- res.end(buffer)
110
  })
111
 
112
  app.listen(port, () => { console.log(`Open http://localhost:${port}`) })
 
1
+ import { createReadStream, promises as fs } from "fs"
2
 
3
  import express from "express"
4
 
5
+ import { VideoTask, VideoSequenceRequest } from "./types.mts"
6
+ import { requestToTask } from "./services/requestToTask.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
 
13
  const app = express()
14
  const port = 7860
15
 
16
  app.use(express.json())
17
 
18
+ app.post("/", async (req, res) => {
19
+ const request = req.body as VideoSequenceRequest
20
+
21
+ const token = `${request.token || ""}`
 
 
22
  if (token !== process.env.VS_SECRET_ACCESS_TOKEN) {
23
  console.log("couldn't find access token in the query")
24
+ res.status(401)
25
+ res.write(JSON.stringify({ error: "invalid token" }))
26
  res.end()
27
  return
28
  }
29
 
30
+ let task: VideoTask = null
31
+
32
+ console.log(`creating task from request..`)
33
+ try {
34
+ task = await requestToTask(request)
35
+ } catch (err) {
36
+ console.error(`failed to create task: ${task}`)
37
+ res.status(400)
38
+ res.write(JSON.stringify({ error: "query seems to be malformed" }))
39
  res.end()
40
  return
41
  }
42
 
43
+ console.log(`saving task ${task.id}`)
44
+ try {
45
+ await savePendingTask(task)
46
+ res.status(200)
47
+ res.write(JSON.stringify(task))
48
+ res.end()
49
+ } catch (err) {
50
+ console.error(err)
51
+ res.status(500)
52
+ res.write(JSON.stringify({ error: "couldn't save the task" }))
53
+ res.end()
54
+ }
55
+ })
56
 
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()
65
+ } catch (err) {
66
+ console.error(err)
67
+ res.status(404)
68
+ res.write(JSON.stringify({ error: "couldn't find this task" }))
69
+ res.end()
70
+ }
71
+ })
72
 
73
+ app.get("/video/:id\.mp4", async (req, res) => {
74
+ if (!req.params.id) {
75
+ res.status(400)
76
+ res.write(JSON.stringify({ error: "please provide a valid video id" }))
77
+ res.end()
78
+ return
79
+ }
 
 
 
80
 
81
+ let task: VideoTask = null
82
+
83
+ try {
84
+ task = await getTask(req.params.id)
85
+ console.log("returning result to user..")
86
+
87
+ const filePath = task.finalFilePath || task.tmpFilePath || ''
88
+ if (!filePath) {
89
+ res.status(400)
90
+ res.write(JSON.stringify({ error: "video exists, but cannot be previewed yet" }))
91
+ res.end()
92
+ return
93
+ }
94
+ } catch (err) {
95
+ res.status(404)
96
+ res.write(JSON.stringify({ error: "this video doesn't exist" }))
97
+ res.end()
98
+ return
99
+ }
100
 
101
+ // file path exists, let's try to read it
102
+ try {
103
+ // do we need this?
104
+ // res.status(200)
105
+ // res.setHeader("Content-Type", "media/mp4")
106
+ console.log(`creating a video read stream from ${filePath}`)
107
+ const stream = createReadStream(filePath)
 
 
 
108
 
109
+ stream.on('close', () => {
110
+ console.log(`finished streaming the video`)
111
+ res.end()
112
+ })
113
+
114
+ stream.pipe(res)
115
+ } catch (err) {
116
+ console.error(`failed to read the video file at ${filePath}: ${err}`)
117
+ res.status(500)
118
+ res.write(JSON.stringify({ error: "failed to read the video file" }))
119
+ res.end()
120
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  })
122
 
123
  app.listen(port, () => { console.log(`Open http://localhost:${port}`) })
src/main.mts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPendingTasks } from "./database/getPendingTasks.mts"
2
+
3
+ export const main = async () => {
4
+ const tasks = await getPendingTasks()
5
+ if (!tasks.length) {
6
+ setTimeout(() => {
7
+ main()
8
+ }, 500)
9
+ return
10
+ }
11
+
12
+ console.log(`there are ${tasks.length} pending tasks`)
13
+
14
+ setTimeout(() => {
15
+ main()
16
+ }, 1000)
17
+ }
src/services/processTask.mts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { VideoTask } from "../types.mts";
2
+
3
+ export const processTask = async (task: VideoTask) => {
4
+
5
+ }
src/services/requestToTask.mts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { v4 as uuidv4 } from "uuid"
2
+
3
+ // convert a request (which might be invalid)
4
+
5
+ import { VideoSequenceRequest, VideoTask } from "../types.mts"
6
+ import { generateSeed } from "./generateSeed.mts"
7
+ import { getValidNumber } from "../utils/getValidNumber.mts"
8
+ import { getValidResolution } from "../utils/getValidResolution.mts"
9
+
10
+ // into a valid task
11
+ export const requestToTask = async (request: VideoSequenceRequest): Promise<VideoTask> => {
12
+
13
+ const task: VideoTask = {
14
+ // ------------ VideoSequenceMeta -------------
15
+ id: uuidv4(),
16
+
17
+ // describe the whole movie
18
+ videoPrompt: `${request.sequence.videoPrompt || ''}`,
19
+
20
+ // describe the background audio (crowd, birds, wind, sea etc..)
21
+ backgroundAudioPrompt: `${request.sequence.backgroundAudioPrompt || ''}`,
22
+
23
+ // describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
24
+ foregroundAudioPrompt: `${request.sequence.foregroundAudioPrompt || ''}`,
25
+
26
+ // describe the main actor visible in the shot (optional)
27
+ actorPrompt: `${request.sequence.actorPrompt || ''}`,
28
+
29
+ // describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
30
+ actorVoicePrompt: `${request.sequence.actorVoicePrompt || ''}`,
31
+
32
+ // describe the main actor dialogue line
33
+ actorDialoguePrompt: `${request.sequence.actorDialoguePrompt || ''}`,
34
+
35
+ seed: getValidNumber(request.sequence.seed, 0, 4294967295, generateSeed()),
36
+
37
+ upscale: request.sequence.upscale === true,
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
+
45
+ resolution: getValidResolution(request.sequence.resolution),
46
+
47
+ outroTransition: 'staticfade',
48
+ outroDurationMs: 3000,
49
+
50
+ // ---------- VideoSequenceData ---------
51
+ nbCompletedShots: 0,
52
+ nbTotalShots: 0,
53
+ progressPercent: 0,
54
+ completedAt: null,
55
+ completed: false,
56
+ error: '',
57
+ tmpFilePath: '',
58
+ finalFilePath: '',
59
+
60
+ // ------- the VideoShot -----
61
+
62
+ shots: [],
63
+ }
64
+
65
+
66
+ // optional background audio prompt
67
+ const backgroundAudioPrompt = `${query.backgroundAudioPrompt || ""}`
68
+
69
+ // optional foreground audio prompt
70
+ const foregroundAudioPrompt = `${query.foregroundAudioPrompt || ""}`
71
+
72
+ // optional seed
73
+ const defaultSeed = generateSeed()
74
+ const seedStr = getValidNumber(Number(`${query.seed || defaultSeed}`)
75
+ const maybeSeed = Number(seedStr)
76
+ const seed = isNaN(maybeSeed) || ! isFinite(maybeSeed) ? defaultSeed : maybeSeed
77
+
78
+ // in production we want those ON by default
79
+ const upscale = `${query.upscale || "true"}` === "true"
80
+ const interpolate = `${query.upscale || "true"}` === "true"
81
+ const noise = `${query.noise || "true"}` === "true"
82
+
83
+
84
+ const defaultDuration = 3
85
+ const maxDuration = 5
86
+ const durationStr = Number(`${query.duration || defaultDuration}`)
87
+ const maybeDuration = Number(durationStr)
88
+ const duration = Math.min(maxDuration, Math.max(1, isNaN(maybeDuration) || !isFinite(maybeDuration) ? defaultDuration : maybeDuration))
89
+
90
+ const defaultSteps = 35
91
+ const stepsStr = Number(`${query.steps || defaultSteps}`)
92
+ const maybeSteps = Number(stepsStr)
93
+ const nbSteps = Math.min(60, Math.max(1, isNaN(maybeSteps) || !isFinite(maybeSteps) ? defaultSteps : maybeSteps))
94
+
95
+ // const frames per second
96
+ const defaultFps = 24
97
+ const fpsStr = Number(`${query.fps || defaultFps}`)
98
+ const maybeFps = Number(fpsStr)
99
+ const nbFrames = Math.min(60, Math.max(8, isNaN(maybeFps) || !isFinite(maybeFps) ? defaultFps : maybeFps))
100
+
101
+ const defaultResolution = 576
102
+ const resolutionStr = Number(`${query.resolution || defaultResolution}`)
103
+ const maybeResolution = Number(resolutionStr)
104
+ const resolution = Math.min(1080, Math.max(256, isNaN(maybeResolution) || !isFinite(maybeResolution) ? defaultResolution : maybeResolution))
105
+
106
+ const actorPrompt = `${query.actorPrompt || ""}`
107
+
108
+ const actorVoicePrompt = `${query.actorVoicePrompt || ""}`
109
+
110
+ const actorDialoguePrompt = `${query.actorDialoguePrompt || ""}`
111
+
112
+
113
+
114
+ return task
115
+ }
src/tests/checkStatus.mts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { videoId, server } from "./config.mts"
2
+
3
+ console.log(`checking status of video ${videoId}`)
4
+ const response = await fetch(`${server}/${videoId}`, {
5
+ method: "GET",
6
+ headers: {
7
+ "Accept": "application/json",
8
+ }
9
+ });
10
+
11
+ console.log('response:', response)
12
+ const task = await response.json()
13
+
14
+ console.log("task:", JSON.stringify(task, null, 2))
src/tests/config.mts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const server = "http://localhost:7860"
2
+
3
+ export const videoId = "9a9009a5-b960-49a2-bed2-baf0423cc907"
src/{test.mts β†’ tests/downloadVideo.mts} RENAMED
@@ -1,23 +1,17 @@
1
  import { promises as fs } from "node:fs"
2
 
 
3
 
4
- console.log('generating shot..')
5
- const response = await fetch("http://localhost:7860/shot", {
6
- method: "POST",
7
- headers: {
8
- "Accept": "application/json",
9
- "Content-Type": "application/json"
10
- },
11
- body: JSON.stringify({
12
- token: process.env.VS_SECRET_ACCESS_TOKEN,
13
- shotPrompt: "video of a dancing cat"
14
- })
15
  });
16
 
17
  console.log('response:', response)
18
  const buffer = await (response as any).buffer()
19
 
20
- fs.writeFile(`./test-juju.mp4`, buffer)
21
 
22
  // if called from an API, we ΓΉight want to use streams instead:
23
  // https://stackoverflow.com/questions/15713424/how-can-i-download-a-video-mp4-file-using-node-js
 
1
  import { promises as fs } from "node:fs"
2
 
3
+ import { videoId, server } from "./config.mts"
4
 
5
+ console.log(`trying to download video ${videoId}`)
6
+
7
+ const response = await fetch(`${server}/${videoId}.mp4`, {
8
+ method: "GET",
 
 
 
 
 
 
 
9
  });
10
 
11
  console.log('response:', response)
12
  const buffer = await (response as any).buffer()
13
 
14
+ fs.writeFile(`./${videoId}.mp4`, buffer)
15
 
16
  // if called from an API, we ΓΉight want to use streams instead:
17
  // https://stackoverflow.com/questions/15713424/how-can-i-download-a-video-mp4-file-using-node-js
src/{test2.mts β†’ tests/stuff.mts} RENAMED
@@ -1,4 +1,4 @@
1
- import { generateAudio } from "./services/generateAudio.mts"
2
 
3
 
4
  console.log('generating background audio..')
 
1
+ import { generateAudio } from "../services/generateAudio.mts"
2
 
3
 
4
  console.log('generating background audio..')
src/tests/submitVideo.mts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { server, videoId } from "./config.mts"
2
+
3
+ console.log('submitting a new video..')
4
+ const response = await fetch(`${server}/`, {
5
+ method: "POST",
6
+ headers: {
7
+ "Accept": "application/json",
8
+ "Content-Type": "application/json"
9
+ },
10
+ body: JSON.stringify({
11
+ token: process.env.VS_SECRET_ACCESS_TOKEN,
12
+ sequence: {
13
+ id: videoId,
14
+ },
15
+ shots: []
16
+ })
17
+ });
18
+
19
+
20
+ console.log('response:', response)
21
+ const task = await response.json()
22
+
23
+ console.log("task:", JSON.stringify(task, null, 2))
src/types.mts CHANGED
@@ -1,65 +1,233 @@
1
- export interface Shot {
2
- shotId: string
3
- index: number
4
- lastGenerationAt: string
5
- videoPrompt: string
6
- audioPrompt: string
7
- duration: number // no more than 3 (we don't have the ressources for it)
8
- fps: number // typically 8, 12, 24
9
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- export interface Sequence {
12
- sequenceId: string
13
- skip: boolean
14
- lastGenerationAt: string
15
- videoPrompt: string
16
- audioPrompt: string
17
- channel: string
18
- tags: string[]
19
- shots: Shot[]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
- export interface Database {
23
- version: number
24
- startAtShotId: string
25
- sequences: Sequence[]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
 
28
 
29
- export interface ShotQuery {
30
- token: string
31
- shotPrompt: string
32
- // inputVideo?: string
 
 
33
 
34
  // describe the background audio (crowd, birds, wind, sea etc..)
35
- backgroundAudioPrompt?: string
36
 
37
  // describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
38
- foregroundAudioPrompt?: string
39
 
40
  // describe the main actor visible in the shot (optional)
41
- actorPrompt?: string
42
 
43
  // describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
44
- actorVoicePrompt?: string
45
 
46
  // describe the main actor dialogue line
47
- actorDialoguePrompt?: string
 
 
 
 
 
48
 
49
- seed?: number
50
- upscale?: boolean
51
 
52
- noise?: boolean // add movie noise
53
 
54
- duration?: number
55
- steps?: number
 
 
 
56
 
57
- fps?: number // 8, 12, 24, 30, 60
58
 
59
- resolution?: number // 256, 512, 576, 720, 1080
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
- export interface Job {
63
- startedAt: string
64
- query: ShotQuery
65
  }
 
1
+ export type VideoTransition =
2
+ | 'dissolve'
3
+ | 'bookflip'
4
+ | 'bounce'
5
+ | 'bowtiehorizontal'
6
+ | 'bowtievertical'
7
+ | 'bowtiewithparameter'
8
+ | 'butterflywavescrawler'
9
+ | 'circlecrop'
10
+ | 'colourdistance'
11
+ | 'crazyparametricfun'
12
+ | 'crosszoom'
13
+ | 'directional'
14
+ | 'directionalscaled'
15
+ | 'doomscreentransition'
16
+ | 'dreamy'
17
+ | 'dreamyzoom'
18
+ | 'edgetransition'
19
+ | 'filmburn'
20
+ | 'filmburnglitchdisplace'
21
+ | 'glitchmemories'
22
+ | 'gridflip'
23
+ | 'horizontalclose'
24
+ | 'horizontalopen'
25
+ | 'invertedpagecurl'
26
+ | 'leftright'
27
+ | 'linearblur'
28
+ | 'mosaic'
29
+ | 'overexposure'
30
+ | 'polkadotscurtain'
31
+ | 'radial'
32
+ | 'rectangle'
33
+ | 'rectanglecrop'
34
+ | 'rolls'
35
+ | 'rotatescalevanish'
36
+ | 'simplezoom'
37
+ | 'simplezoomout'
38
+ | 'slides'
39
+ | 'staticfade'
40
+ | 'stereoviewer'
41
+ | 'swirl'
42
+ | 'tvstatic'
43
+ | 'topbottom'
44
+ | 'verticalclose'
45
+ | 'verticalopen'
46
+ | 'waterdrop'
47
+ | 'waterdropzoomincircles'
48
+ | 'zoomleftwipe'
49
+ | 'zoomrigthwipe'
50
+ | 'angular'
51
+ | 'burn'
52
+ | 'cannabisleaf'
53
+ | 'circle'
54
+ | 'circleopen'
55
+ | 'colorphase'
56
+ | 'coordfromin'
57
+ | 'crosshatch'
58
+ | 'crosswarp'
59
+ | 'cube'
60
+ | 'directionaleasing'
61
+ | 'directionalwarp'
62
+ | 'directionalwipe'
63
+ | 'displacement'
64
+ | 'doorway'
65
+ | 'fade'
66
+ | 'fadecolor'
67
+ | 'fadegrayscale'
68
+ | 'flyeye'
69
+ | 'heart'
70
+ | 'hexagonalize'
71
+ | 'kaleidoscope'
72
+ | 'luma'
73
+ | 'luminance_melt'
74
+ | 'morph'
75
+ | 'mosaic_transition'
76
+ | 'multiply_blend'
77
+ | 'perlin'
78
+ | 'pinwheel'
79
+ | 'pixelize'
80
+ | 'polar_function'
81
+ | 'powerkaleido'
82
+ | 'randomnoisex'
83
+ | 'randomsquares'
84
+ | 'ripple'
85
+ | 'rotatetransition'
86
+ | 'rotate_scale_fade'
87
+ | 'scalein'
88
+ | 'squareswire'
89
+ | 'squeeze'
90
+ | 'static_wipe'
91
+ | 'swap'
92
+ | 'tangentmotionblur'
93
+ | 'undulatingburnout'
94
+ | 'wind'
95
+ | 'windowblinds'
96
+ | 'windowslice'
97
+ | 'wipedown'
98
+ | 'wipeleft'
99
+ | 'wiperight'
100
+ | 'wipeup'
101
+ | 'x_axistranslation'
102
 
103
+
104
+ export interface VideoShotMeta {
105
+ // must be unique
106
+ id: string
107
+
108
+ shotPrompt: string
109
+ // inputVideo?: string
110
+
111
+ // describe the background audio (crowd, birds, wind, sea etc..)
112
+ backgroundAudioPrompt: string
113
+
114
+ // describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
115
+ foregroundAudioPrompt: string
116
+
117
+ // describe the main actor visible in the shot (optional)
118
+ actorPrompt: string
119
+
120
+ // describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
121
+ actorVoicePrompt: string
122
+
123
+ // describe the main actor dialogue line
124
+ actorDialoguePrompt: string
125
+
126
+ seed: number
127
+ upscale: boolean
128
+
129
+ noise: boolean // add movie noise
130
+
131
+ durationMs: number // in milliseconds
132
+ steps: number
133
+
134
+ fps: number // 8, 12, 24, 30, 60
135
+
136
+ resolution: number // 256, 512, 576, 720, 1080
137
+
138
+ introTransition: VideoTransition
139
+ introDurationMs: number // in milliseconds
140
+
141
+ // for internal use
142
+ hasGeneratedVideo: boolean
143
+ hasUpscaledVideo: boolean
144
+ hasGeneratedBackgroundAudio: boolean
145
+ hasGeneratedForegroundAudio: boolean
146
+ hasGeneratedActor: boolean
147
+ hasInterpolatedVideo: boolean
148
+ hasAddedAudio: boolean
149
+ hasPostProcessedVideo: boolean
150
  }
151
 
152
+
153
+ export interface VideoShotData {
154
+ hasGeneratedVideo: boolean
155
+ hasUpscaledVideo: boolean
156
+ hasGeneratedBackgroundAudio: boolean
157
+ hasGeneratedForegroundAudio: boolean
158
+ hasGeneratedActor: boolean
159
+ hasInterpolatedVideo: boolean
160
+ hasAddedAudio: boolean
161
+ hasPostProcessedVideo: boolean
162
+ nbCompletedSteps: number
163
+ nbTotalSteps: number
164
+ progressPercent: number
165
+ completedAt: string
166
+ completed: boolean
167
+ error: string
168
+ tmpFilePath: string
169
+ finalFilePath: string
170
  }
171
 
172
+ export type VideoShot = VideoShotMeta & VideoShotData
173
 
174
+ export interface VideoSequenceMeta {
175
+ // must be unique
176
+ id: string
177
+
178
+ // describe the whole movie
179
+ videoPrompt: string
180
 
181
  // describe the background audio (crowd, birds, wind, sea etc..)
182
+ backgroundAudioPrompt: string
183
 
184
  // describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
185
+ foregroundAudioPrompt: string
186
 
187
  // describe the main actor visible in the shot (optional)
188
+ actorPrompt: string
189
 
190
  // describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
191
+ actorVoicePrompt: string
192
 
193
  // describe the main actor dialogue line
194
+ actorDialoguePrompt: string
195
+
196
+ seed: number
197
+ upscale: boolean
198
+
199
+ noise: boolean // add movie noise
200
 
201
+ steps: number
 
202
 
203
+ fps: number // 8, 12, 24, 30, 60
204
 
205
+ resolution: string // 256, 512, 576, 720, 1080
206
+
207
+ outroTransition: VideoTransition
208
+ outroDurationMs: number
209
+ }
210
 
 
211
 
212
+ export interface VideoSequenceData {
213
+ nbCompletedShots: number
214
+ nbTotalShots: number
215
+ progressPercent: number
216
+ completedAt: string
217
+ completed: boolean
218
+ error: string
219
+ tmpFilePath: string
220
+ finalFilePath: string
221
+ }
222
+
223
+ export type VideoSequence = VideoSequenceMeta & VideoSequenceData
224
+
225
+ export type VideoSequenceRequest = {
226
+ token: string
227
+ sequence: VideoSequenceMeta
228
+ shots: VideoShotMeta[]
229
  }
230
 
231
+ export type VideoTask = VideoSequence & {
232
+ shots: VideoShot[]
 
233
  }
src/utils/getValidNumber.mts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export const getValidNumber = (something: any, minValue: number, maxValue: number, defaultValue: number) => {
2
+ const strValue = `${something || defaultValue}`
3
+ const numValue = Number(strValue)
4
+ const isValid = !isNaN(numValue) && isFinite(numValue)
5
+ if (!isValid) {
6
+ return defaultValue
7
+ }
8
+ return Math.max(minValue, Math.min(maxValue, numValue))
9
+
10
+ }
src/utils/getValidResolution.mts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getValidNumber } from "./getValidNumber.mts"
2
+
3
+ export const getValidResolution = (something: any) => {
4
+ const strValue = `${something || ''}`
5
+ const chunks = strValue.split('x')
6
+ if (chunks.length < 2) {
7
+ throw new Error('Invalid resolution (should be written like "1280x720" etc)')
8
+ }
9
+
10
+ const [widthStr, heightStr] = chunks
11
+ const width = getValidNumber(widthStr, 256, 1280, 1280)
12
+ const height = getValidNumber(widthStr, 256, 720, 720)
13
+
14
+ return `${width}x${height}`
15
+ }