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 |
|