Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
Β·
5dfc565
1
Parent(s):
deae345
ok! now, time to debug and build the frontend..
Browse files- .gitignore +0 -2
- database/completed/README.md +0 -0
- database/pending/README.md +0 -1
- package.json +1 -1
- src/config.mts +3 -3
- src/index.mts +34 -6
- src/initFolders.mts +12 -0
- src/main.mts +6 -2
- src/{services β production}/addAudioToVideo.mts +6 -8
- src/production/assembleShots.mts +53 -0
- src/{services β production}/generateActor.mts +0 -0
- src/{services β production}/generateAudio.mts +13 -11
- src/{services β production}/generateAudioLegacy.mts +1 -1
- src/{services β production}/generateShot.mts +5 -5
- src/{services β production}/generateVideo.mts +1 -1
- src/{services β production}/generateVoice.mts +2 -2
- src/{services β production}/interpolateVideo.mts +13 -6
- src/{services β production}/interpolateVideoLegacy.mts +2 -2
- src/{services β production}/mergeAudio.mts +0 -0
- src/{services β production}/postInterpolation.mts +12 -12
- src/{services β production}/upscaleVideo.mts +15 -25
- src/scheduler/deleteTask.mts +33 -0
- src/{database β scheduler}/getCompletedTasks.mts +1 -1
- src/{database β scheduler}/getPendingTasks.mts +1 -1
- src/{database β scheduler}/getTask.mts +1 -1
- src/scheduler/processTask.mts +246 -0
- src/{database β scheduler}/readTask.mts +0 -0
- src/{database β scheduler}/readTasks.mts +0 -0
- src/{database β scheduler}/saveCompletedTask.mts +2 -2
- src/{database β scheduler}/savePendingTask.mts +0 -0
- src/{database β scheduler}/updatePendingTask.mts +0 -0
- src/services/processTask.mts +0 -68
- src/types.mts +12 -18
- src/utils/copyVideoFromPendingToCompleted.mts +15 -0
- src/utils/copyVideoFromTmpToCompleted.mts +20 -0
- src/utils/copyVideoFromTmpToPending.mts +21 -0
- src/utils/createDirIfNeeded.mts +7 -0
- src/utils/deleteFileIfExists.mts +13 -0
- src/{services/downloadVideo.mts β utils/downloadFileToTmp.mts} +7 -8
- src/{services β utils}/generateSeed.mts +0 -0
- src/utils/moveFile.mts +15 -0
- src/utils/moveFileFromTmpToPending.mts +18 -0
- src/utils/moveVideoFromPendingToCompleted.mts +14 -0
- src/utils/moveVideoFromTmpToCompleted.mts +18 -0
- src/utils/parseShotRequest.mts +9 -5
- 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:
|
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
|
10 |
-
export const
|
11 |
-
export const
|
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,
|
|
|
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 "./
|
8 |
-
import { getTask } from "./
|
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 |
-
|
|
|
|
|
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 {
|
2 |
-
import {
|
|
|
|
|
|
|
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 |
-
)
|
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(
|
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
|
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 |
-
|
|
|
|
|
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 |
-
|
46 |
-
|
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 |
-
|
|
|
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 "
|
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 {
|
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 "
|
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
|
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 |
-
|
139 |
interpolationSteps,
|
140 |
interpolatedFramesPerSecond
|
141 |
)
|
@@ -194,7 +194,7 @@ export const generateShot = async ({
|
|
194 |
audioFileName = foregroundAudioFileName
|
195 |
}
|
196 |
|
197 |
-
await addAudioToVideo(
|
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 "
|
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 {
|
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
|
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
|
4 |
import tmpDir from "temp-dir"
|
5 |
-
import
|
|
|
|
|
|
|
|
|
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(
|
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 |
-
|
|
|
|
|
|
|
51 |
|
52 |
-
|
|
|
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 {
|
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
|
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,
|
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
|
22 |
-
|
|
|
|
|
|
|
|
|
23 |
// compute a ratio ex. 0.3 = 30% of the total length
|
24 |
-
const durationRatio =
|
|
|
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
|
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
|
2 |
-
import fs from 'node:fs'
|
3 |
|
4 |
-
import
|
5 |
-
import
|
6 |
-
import
|
|
|
|
|
|
|
|
|
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(
|
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 |
-
//
|
63 |
-
|
64 |
-
|
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 |
-
|
|
|
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 "
|
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 "
|
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 "
|
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
|
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
|
2 |
-
import fs from
|
3 |
-
import { pendingVideosDirFilePath } from '../config.mts'
|
4 |
|
5 |
-
|
6 |
|
7 |
-
|
|
|
|
|
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
|
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 "
|
7 |
import { getValidNumber } from "./getValidNumber.mts"
|
8 |
-
import { shotFormatVersion } from "../
|
9 |
|
10 |
export const parseShotRequest = async (sequence: VideoSequence, maybeShotMeta: VideoShotMeta): Promise<VideoShot> => {
|
|
|
|
|
|
|
11 |
|
12 |
const shot: VideoShot = {
|
13 |
-
id
|
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,
|
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 {
|
|
|
11 |
|
12 |
|
13 |
export const parseVideoRequest = async (request: VideoSequenceRequest): Promise<VideoTask> => {
|
|
|
|
|
|
|
14 |
|
15 |
const task: VideoTask = {
|
16 |
// ------------ VideoSequenceMeta -------------
|
17 |
-
id
|
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,
|
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 |
-
|
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 |
|