Spaces:
Paused
Paused
Julian Bilcke
commited on
Commit
·
cd0e411
1
Parent(s):
ba53841
added support for parallelism
Browse files- src/main.mts +20 -7
- src/production/generateAudio.mts +45 -31
- src/production/generateAudioLegacy.mts +39 -22
- src/production/generateVideo.mts +39 -22
- src/production/generateVoice.mts +46 -30
- src/production/interpolateVideo.mts +48 -33
- src/production/interpolateVideoLegacy.mts +41 -26
- src/production/renderStaticScene.mts +4 -1
- src/types.mts +13 -0
src/main.mts
CHANGED
|
@@ -7,9 +7,10 @@ export const main = async () => {
|
|
| 7 |
|
| 8 |
const videos = await getPendingVideos()
|
| 9 |
if (!videos.length) {
|
|
|
|
| 10 |
setTimeout(() => {
|
| 11 |
main()
|
| 12 |
-
},
|
| 13 |
return
|
| 14 |
}
|
| 15 |
|
|
@@ -17,12 +18,24 @@ export const main = async () => {
|
|
| 17 |
|
| 18 |
sortPendingVideosByLeastCompletedFirst(videos)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
-
console.log(`processed ${videos.length} videos`)
|
| 24 |
|
| 25 |
-
setTimeout(() => {
|
| 26 |
-
main()
|
| 27 |
-
}, 1000)
|
| 28 |
}
|
|
|
|
| 7 |
|
| 8 |
const videos = await getPendingVideos()
|
| 9 |
if (!videos.length) {
|
| 10 |
+
console.log(`no job to process.. going to try in 200 ms`)
|
| 11 |
setTimeout(() => {
|
| 12 |
main()
|
| 13 |
+
}, 200)
|
| 14 |
return
|
| 15 |
}
|
| 16 |
|
|
|
|
| 18 |
|
| 19 |
sortPendingVideosByLeastCompletedFirst(videos)
|
| 20 |
|
| 21 |
+
let somethingFailed = ""
|
| 22 |
+
await Promise.all(videos.map(async video => {
|
| 23 |
+
try {
|
| 24 |
+
const result = await processVideo(video)
|
| 25 |
+
return result
|
| 26 |
+
} catch (err) {
|
| 27 |
+
somethingFailed = `${err}`
|
| 28 |
+
// a video failed.. no big deal
|
| 29 |
+
return Promise.resolve(somethingFailed)
|
| 30 |
+
}
|
| 31 |
+
}))
|
| 32 |
+
|
| 33 |
+
if (somethingFailed) {
|
| 34 |
+
console.error(`one of the jobs failed: ${somethingFailed}, let's wait 3 seconds`)
|
| 35 |
+
setTimeout(() => { main() }, 3000)
|
| 36 |
+
} else {
|
| 37 |
+
console.log(`successfully worked on the jobs, let's immediately loop`)
|
| 38 |
+
setTimeout(() => { main() }, 50)
|
| 39 |
}
|
|
|
|
| 40 |
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
src/production/generateAudio.mts
CHANGED
|
@@ -1,62 +1,76 @@
|
|
| 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.VC_AUDIO_GENERATION_SPACE_API_URL
|
| 12 |
]
|
| 13 |
|
| 14 |
// TODO we should use an inference endpoint instead
|
| 15 |
export async function generateAudio(prompt: string, audioFileName: string) {
|
| 16 |
-
const instance = instances.shift()
|
| 17 |
-
instances.push(instance)
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
| 23 |
|
| 24 |
try {
|
| 25 |
-
const
|
|
|
|
| 26 |
|
| 27 |
-
await
|
| 28 |
-
|
|
|
|
| 29 |
})
|
| 30 |
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
const submitButton = await page.$("button.lg")
|
| 39 |
|
| 40 |
-
|
| 41 |
-
await submitButton.click()
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
|
|
|
|
| 49 |
|
| 50 |
-
// it is always a good idea to download to a tmp dir before saving to the pending dir
|
| 51 |
-
// because there is always a risk that the download will fail
|
| 52 |
-
|
| 53 |
-
const tmpFileName = `${uuidv4()}.mp4`
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
} catch (err) {
|
| 58 |
throw err
|
| 59 |
} finally {
|
| 60 |
-
|
| 61 |
}
|
| 62 |
-
}
|
|
|
|
|
|
|
|
|
|
| 1 |
import { v4 as uuidv4 } from "uuid"
|
|
|
|
| 2 |
import puppeteer from "puppeteer"
|
| 3 |
|
| 4 |
import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
|
| 5 |
import { moveFileFromTmpToPending } from "../utils/moveFileFromTmpToPending.mts"
|
| 6 |
|
| 7 |
+
export const state = {
|
| 8 |
+
load: 0,
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
const instances: string[] = [
|
| 12 |
process.env.VC_AUDIO_GENERATION_SPACE_API_URL
|
| 13 |
]
|
| 14 |
|
| 15 |
// TODO we should use an inference endpoint instead
|
| 16 |
export async function generateAudio(prompt: string, audioFileName: string) {
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
if (state.load === instances.length) {
|
| 19 |
+
throw new Error(`all audio generation servers are busy, try again later..`)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
state.load += 1
|
| 23 |
|
| 24 |
try {
|
| 25 |
+
const instance = instances.shift()
|
| 26 |
+
instances.push(instance)
|
| 27 |
|
| 28 |
+
const browser = await puppeteer.launch({
|
| 29 |
+
headless: true,
|
| 30 |
+
protocolTimeout: 120000,
|
| 31 |
})
|
| 32 |
|
| 33 |
+
try {
|
| 34 |
+
const page = await browser.newPage()
|
| 35 |
|
| 36 |
+
await page.goto(instance, {
|
| 37 |
+
waitUntil: "networkidle2",
|
| 38 |
+
})
|
| 39 |
|
| 40 |
+
await new Promise(r => setTimeout(r, 3000))
|
| 41 |
|
| 42 |
+
const firstTextboxInput = await page.$('input[data-testid="textbox"]')
|
|
|
|
| 43 |
|
| 44 |
+
await firstTextboxInput.type(prompt)
|
|
|
|
| 45 |
|
| 46 |
+
// console.log("looking for the button to submit")
|
| 47 |
+
const submitButton = await page.$("button.lg")
|
| 48 |
+
|
| 49 |
+
// console.log("clicking on the button")
|
| 50 |
+
await submitButton.click()
|
| 51 |
|
| 52 |
+
await page.waitForSelector("a[download]", {
|
| 53 |
+
timeout: 120000, // no need to wait for too long, generation is quick
|
| 54 |
+
})
|
| 55 |
|
| 56 |
+
const audioRemoteUrl = await page.$$eval("a[download]", el => el.map(x => x.getAttribute("href"))[0])
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
// it is always a good idea to download to a tmp dir before saving to the pending dir
|
| 60 |
+
// because there is always a risk that the download will fail
|
| 61 |
+
|
| 62 |
+
const tmpFileName = `${uuidv4()}.mp4`
|
| 63 |
+
|
| 64 |
+
await downloadFileToTmp(audioRemoteUrl, tmpFileName)
|
| 65 |
+
await moveFileFromTmpToPending(tmpFileName, audioFileName)
|
| 66 |
+
} catch (err) {
|
| 67 |
+
throw err
|
| 68 |
+
} finally {
|
| 69 |
+
await browser.close()
|
| 70 |
+
}
|
| 71 |
} catch (err) {
|
| 72 |
throw err
|
| 73 |
} finally {
|
| 74 |
+
state.load -= 1
|
| 75 |
}
|
| 76 |
+
}
|
src/production/generateAudioLegacy.mts
CHANGED
|
@@ -2,6 +2,10 @@ import { client } from '@gradio/client'
|
|
| 2 |
|
| 3 |
import { generateSeed } from "../utils/generateSeed.mts"
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
const instances: string[] = [
|
| 6 |
process.env.VC_AUDIO_GENERATION_SPACE_API_URL
|
| 7 |
]
|
|
@@ -11,25 +15,38 @@ export const generateAudio = async (prompt: string, options?: {
|
|
| 11 |
nbFrames: number;
|
| 12 |
nbSteps: number;
|
| 13 |
}) => {
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import { generateSeed } from "../utils/generateSeed.mts"
|
| 4 |
|
| 5 |
+
export const state = {
|
| 6 |
+
load: 0
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
const instances: string[] = [
|
| 10 |
process.env.VC_AUDIO_GENERATION_SPACE_API_URL
|
| 11 |
]
|
|
|
|
| 15 |
nbFrames: number;
|
| 16 |
nbSteps: number;
|
| 17 |
}) => {
|
| 18 |
+
|
| 19 |
+
if (state.load === instances.length) {
|
| 20 |
+
throw new Error(`all audio generation servers are busy, try again later..`)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
state.load += 1
|
| 24 |
+
|
| 25 |
+
try {
|
| 26 |
+
const seed = options?.seed || generateSeed()
|
| 27 |
+
const nbFrames = options?.nbFrames || 24 // we can go up to 48 frames, but then upscaling quill require too much memory!
|
| 28 |
+
const nbSteps = options?.nbSteps || 35
|
| 29 |
+
|
| 30 |
+
const instance = instances.shift()
|
| 31 |
+
instances.push(instance)
|
| 32 |
+
|
| 33 |
+
const api = await client(instance, {
|
| 34 |
+
hf_token: `${process.env.VC_HF_API_TOKEN}` as any
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
const rawResponse = await api.predict('/run', [
|
| 38 |
+
prompt, // string in 'Prompt' Textbox component
|
| 39 |
+
seed, // number (numeric value between 0 and 2147483647) in 'Seed' Slider component
|
| 40 |
+
nbFrames, // 24 // it is the nb of frames per seconds I think?
|
| 41 |
+
nbSteps, // 10, (numeric value between 10 and 50) in 'Number of inference steps' Slider component
|
| 42 |
+
]) as any
|
| 43 |
+
|
| 44 |
+
const { name } = rawResponse?.data?.[0]?.[0] as { name: string, orig_name: string }
|
| 45 |
+
|
| 46 |
+
return `${instance}/file=${name}`
|
| 47 |
+
} catch (err) {
|
| 48 |
+
throw err
|
| 49 |
+
} finally {
|
| 50 |
+
state.load -= 1
|
| 51 |
+
}
|
| 52 |
+
}
|
src/production/generateVideo.mts
CHANGED
|
@@ -2,6 +2,10 @@ import { client } from "@gradio/client"
|
|
| 2 |
|
| 3 |
import { generateSeed } from "../utils/generateSeed.mts"
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
// we don't use replicas yet, because it ain't easy to get their hostname
|
| 6 |
const instances: string[] = [
|
| 7 |
`${process.env.VC_ZEROSCOPE_SPACE_API_URL_1 || ""}`,
|
|
@@ -14,27 +18,40 @@ export const generateVideo = async (prompt: string, options?: {
|
|
| 14 |
nbFrames: number;
|
| 15 |
nbSteps: number;
|
| 16 |
}) => {
|
| 17 |
-
const seed = options?.seed || generateSeed()
|
| 18 |
-
const nbFrames = options?.nbFrames || 24 // we can go up to 48 frames, but then upscaling quill require too much memory!
|
| 19 |
-
const nbSteps = options?.nbSteps || 35
|
| 20 |
-
|
| 21 |
-
const instance = instances.shift()
|
| 22 |
-
instances.push(instance)
|
| 23 |
-
|
| 24 |
-
const api = await client(instance, {
|
| 25 |
-
hf_token: `${process.env.VC_HF_API_TOKEN}` as any
|
| 26 |
-
})
|
| 27 |
-
|
| 28 |
-
const rawResponse = await api.predict('/run', [
|
| 29 |
-
prompt, // string in 'Prompt' Textbox component
|
| 30 |
-
seed, // number (numeric value between 0 and 2147483647) in 'Seed' Slider component
|
| 31 |
-
nbFrames, // 24 // it is the nb of frames per seconds I think?
|
| 32 |
-
nbSteps, // 10, (numeric value between 10 and 50) in 'Number of inference steps' Slider component
|
| 33 |
-
]) as any
|
| 34 |
-
|
| 35 |
-
// console.log("rawResponse:", rawResponse)
|
| 36 |
-
|
| 37 |
-
const { name } = rawResponse?.data?.[0]?.[0] as { name: string, orig_name: string }
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
|
|
|
| 2 |
|
| 3 |
import { generateSeed } from "../utils/generateSeed.mts"
|
| 4 |
|
| 5 |
+
export const state = {
|
| 6 |
+
load: 0,
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
// we don't use replicas yet, because it ain't easy to get their hostname
|
| 10 |
const instances: string[] = [
|
| 11 |
`${process.env.VC_ZEROSCOPE_SPACE_API_URL_1 || ""}`,
|
|
|
|
| 18 |
nbFrames: number;
|
| 19 |
nbSteps: number;
|
| 20 |
}) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
if (state.load === instances.length) {
|
| 23 |
+
throw new Error(`all video generation servers are busy, try again later..`)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
state.load += 1
|
| 27 |
+
|
| 28 |
+
try {
|
| 29 |
+
const seed = options?.seed || generateSeed()
|
| 30 |
+
const nbFrames = options?.nbFrames || 24 // we can go up to 48 frames, but then upscaling quill require too much memory!
|
| 31 |
+
const nbSteps = options?.nbSteps || 35
|
| 32 |
+
|
| 33 |
+
const instance = instances.shift()
|
| 34 |
+
instances.push(instance)
|
| 35 |
+
|
| 36 |
+
const api = await client(instance, {
|
| 37 |
+
hf_token: `${process.env.VC_HF_API_TOKEN}` as any
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
const rawResponse = await api.predict('/run', [
|
| 41 |
+
prompt, // string in 'Prompt' Textbox component
|
| 42 |
+
seed, // number (numeric value between 0 and 2147483647) in 'Seed' Slider component
|
| 43 |
+
nbFrames, // 24 // it is the nb of frames per seconds I think?
|
| 44 |
+
nbSteps, // 10, (numeric value between 10 and 50) in 'Number of inference steps' Slider component
|
| 45 |
+
]) as any
|
| 46 |
+
|
| 47 |
+
// console.log("rawResponse:", rawResponse)
|
| 48 |
+
|
| 49 |
+
const { name } = rawResponse?.data?.[0]?.[0] as { name: string, orig_name: string }
|
| 50 |
+
|
| 51 |
+
return `${instance}/file=${name}`
|
| 52 |
+
} catch (err) {
|
| 53 |
+
throw err
|
| 54 |
+
} finally {
|
| 55 |
+
state.load -= 1
|
| 56 |
+
}
|
| 57 |
}
|
src/production/generateVoice.mts
CHANGED
|
@@ -2,61 +2,77 @@ import puppeteer from "puppeteer"
|
|
| 2 |
|
| 3 |
import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
const instances: string[] = [
|
| 6 |
process.env.VC_VOICE_GENERATION_SPACE_API_URL
|
| 7 |
]
|
| 8 |
|
| 9 |
// TODO we should use an inference endpoint instead
|
| 10 |
export async function generateVoice(prompt: string, voiceFileName: string) {
|
| 11 |
-
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
const browser = await puppeteer.launch({
|
| 17 |
-
headless: true,
|
| 18 |
-
protocolTimeout: 800000,
|
| 19 |
-
})
|
| 20 |
|
| 21 |
try {
|
| 22 |
-
const
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
})
|
| 27 |
|
| 28 |
-
|
|
|
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
const submitButton = await page.$("button.lg")
|
| 36 |
|
| 37 |
-
|
| 38 |
-
await submitButton.click()
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
})
|
| 43 |
|
| 44 |
-
|
|
|
|
| 45 |
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
-
|
| 53 |
|
| 54 |
-
|
| 55 |
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
} catch (err) {
|
| 58 |
throw err
|
| 59 |
} finally {
|
| 60 |
-
|
| 61 |
}
|
| 62 |
}
|
|
|
|
| 2 |
|
| 3 |
import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
|
| 4 |
|
| 5 |
+
export const state = {
|
| 6 |
+
load: 0
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
const instances: string[] = [
|
| 10 |
process.env.VC_VOICE_GENERATION_SPACE_API_URL
|
| 11 |
]
|
| 12 |
|
| 13 |
// TODO we should use an inference endpoint instead
|
| 14 |
export async function generateVoice(prompt: string, voiceFileName: string) {
|
| 15 |
+
if (state.load === instances.length) {
|
| 16 |
+
throw new Error(`all voice generation servers are busy, try again later..`)
|
| 17 |
+
}
|
| 18 |
|
| 19 |
+
state.load += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
try {
|
| 22 |
+
const instance = instances.shift()
|
| 23 |
+
instances.push(instance)
|
| 24 |
+
|
| 25 |
+
console.log("instance:", instance)
|
| 26 |
+
|
| 27 |
+
const browser = await puppeteer.launch({
|
| 28 |
+
headless: true,
|
| 29 |
+
protocolTimeout: 800000,
|
| 30 |
})
|
| 31 |
|
| 32 |
+
try {
|
| 33 |
+
const page = await browser.newPage()
|
| 34 |
|
| 35 |
+
await page.goto(instance, {
|
| 36 |
+
waitUntil: "networkidle2",
|
| 37 |
+
})
|
| 38 |
|
| 39 |
+
await new Promise(r => setTimeout(r, 3000))
|
| 40 |
|
| 41 |
+
const firstTextarea = await page.$('textarea[data-testid="textbox"]')
|
|
|
|
| 42 |
|
| 43 |
+
await firstTextarea.type(prompt)
|
|
|
|
| 44 |
|
| 45 |
+
// console.log("looking for the button to submit")
|
| 46 |
+
const submitButton = await page.$("button.lg")
|
|
|
|
| 47 |
|
| 48 |
+
// console.log("clicking on the button")
|
| 49 |
+
await submitButton.click()
|
| 50 |
|
| 51 |
+
await page.waitForSelector("audio", {
|
| 52 |
+
timeout: 800000, // need to be large enough in case someone else attemps to use our space
|
| 53 |
+
})
|
| 54 |
|
| 55 |
+
const voiceRemoteUrl = await page.$$eval("audio", el => el.map(x => x.getAttribute("src"))[0])
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
console.log({
|
| 59 |
+
voiceRemoteUrl,
|
| 60 |
+
})
|
| 61 |
|
| 62 |
|
| 63 |
+
console.log(`- downloading ${voiceFileName} from ${voiceRemoteUrl}`)
|
| 64 |
|
| 65 |
+
await downloadFileToTmp(voiceRemoteUrl, voiceFileName)
|
| 66 |
|
| 67 |
+
return voiceFileName
|
| 68 |
+
} catch (err) {
|
| 69 |
+
throw err
|
| 70 |
+
} finally {
|
| 71 |
+
await browser.close()
|
| 72 |
+
}
|
| 73 |
} catch (err) {
|
| 74 |
throw err
|
| 75 |
} finally {
|
| 76 |
+
state.load -= 1
|
| 77 |
}
|
| 78 |
}
|
src/production/interpolateVideo.mts
CHANGED
|
@@ -1,66 +1,81 @@
|
|
| 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.VC_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 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
console.log(`warning: interpolateVideo parameter "${steps}" is ignored!`)
|
| 21 |
-
console.log(`warning: interpolateVideo parameter "${fps}" is ignored!`)
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
})
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
await page.goto(instance, { waitUntil: 'networkidle2' })
|
| 34 |
-
|
| 35 |
-
await new Promise(r => setTimeout(r, 3000))
|
| 36 |
|
| 37 |
-
const
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
const submitButton = await page.$('button.lg')
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
} catch (err) {
|
| 62 |
throw err
|
| 63 |
} finally {
|
| 64 |
-
|
| 65 |
}
|
| 66 |
}
|
|
|
|
| 1 |
import path from "node:path"
|
| 2 |
|
| 3 |
import { v4 as uuidv4 } from "uuid"
|
|
|
|
| 4 |
import puppeteer from "puppeteer"
|
| 5 |
|
| 6 |
import { downloadFileToTmp } from "../utils/downloadFileToTmp.mts"
|
| 7 |
import { pendingFilesDirFilePath } from "../config.mts"
|
| 8 |
import { moveFileFromTmpToPending } from "../utils/moveFileFromTmpToPending.mts"
|
| 9 |
|
| 10 |
+
export const state = {
|
| 11 |
+
load: 0
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
const instances: string[] = [
|
| 15 |
process.env.VC_VIDEO_INTERPOLATION_SPACE_API_URL
|
| 16 |
]
|
| 17 |
|
| 18 |
// TODO we should use an inference endpoint instead
|
| 19 |
export async function interpolateVideo(fileName: string, steps: number, fps: number) {
|
| 20 |
+
if (state.load === instances.length) {
|
| 21 |
+
throw new Error(`all video interpolation servers are busy, try again later..`)
|
| 22 |
+
}
|
| 23 |
|
| 24 |
+
state.load += 1
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
try {
|
| 27 |
+
const inputFilePath = path.join(pendingFilesDirFilePath, fileName)
|
| 28 |
|
| 29 |
+
console.log(`interpolating ${fileName}`)
|
| 30 |
+
console.log(`warning: interpolateVideo parameter "${steps}" is ignored!`)
|
| 31 |
+
console.log(`warning: interpolateVideo parameter "${fps}" is ignored!`)
|
|
|
|
| 32 |
|
| 33 |
+
const instance = instances.shift()
|
| 34 |
+
instances.push(instance)
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
const browser = await puppeteer.launch({
|
| 37 |
+
headless: true,
|
| 38 |
+
protocolTimeout: 400000,
|
| 39 |
+
})
|
| 40 |
|
| 41 |
+
try {
|
| 42 |
+
const page = await browser.newPage()
|
| 43 |
+
await page.goto(instance, { waitUntil: 'networkidle2' })
|
| 44 |
+
|
| 45 |
+
await new Promise(r => setTimeout(r, 3000))
|
| 46 |
|
| 47 |
+
const fileField = await page.$('input[type=file]')
|
|
|
|
| 48 |
|
| 49 |
+
// console.log(`uploading file..`)
|
| 50 |
+
await fileField.uploadFile(inputFilePath)
|
| 51 |
|
| 52 |
+
// console.log('looking for the button to submit')
|
| 53 |
+
const submitButton = await page.$('button.lg')
|
| 54 |
+
|
| 55 |
+
// console.log('clicking on the button')
|
| 56 |
+
await submitButton.click()
|
| 57 |
+
|
| 58 |
+
await page.waitForSelector('a[download="interpolated_result.mp4"]', {
|
| 59 |
+
timeout: 400000, // need to be large enough in case someone else attemps to use our space
|
| 60 |
+
})
|
| 61 |
|
| 62 |
+
const interpolatedFileUrl = await page.$$eval('a[download="interpolated_result.mp4"]', el => el.map(x => x.getAttribute("href"))[0])
|
| 63 |
|
| 64 |
+
// it is always a good idea to download to a tmp dir before saving to the pending dir
|
| 65 |
+
// because there is always a risk that the download will fail
|
| 66 |
|
| 67 |
+
const tmpFileName = `${uuidv4()}.mp4`
|
| 68 |
|
| 69 |
+
await downloadFileToTmp(interpolatedFileUrl, tmpFileName)
|
| 70 |
+
await moveFileFromTmpToPending(tmpFileName, fileName)
|
| 71 |
+
} catch (err) {
|
| 72 |
+
throw err
|
| 73 |
+
} finally {
|
| 74 |
+
await browser.close()
|
| 75 |
+
}
|
| 76 |
} catch (err) {
|
| 77 |
throw err
|
| 78 |
} finally {
|
| 79 |
+
state.load -= 1
|
| 80 |
}
|
| 81 |
}
|
src/production/interpolateVideoLegacy.mts
CHANGED
|
@@ -7,35 +7,50 @@ import tmpDir from "temp-dir"
|
|
| 7 |
|
| 8 |
import { downloadFileToTmp } from '../utils/downloadFileToTmp.mts'
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
const instances: string[] = [
|
| 11 |
process.env.VC_VIDEO_INTERPOLATION_SPACE_API_URL
|
| 12 |
]
|
| 13 |
|
| 14 |
export const interpolateVideo = async (fileName: string, steps: number, fps: number) => {
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
|
|
|
| 7 |
|
| 8 |
import { downloadFileToTmp } from '../utils/downloadFileToTmp.mts'
|
| 9 |
|
| 10 |
+
export const state = {
|
| 11 |
+
load: 0
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
const instances: string[] = [
|
| 15 |
process.env.VC_VIDEO_INTERPOLATION_SPACE_API_URL
|
| 16 |
]
|
| 17 |
|
| 18 |
export const interpolateVideo = async (fileName: string, steps: number, fps: number) => {
|
| 19 |
+
if (state.load === instances.length) {
|
| 20 |
+
throw new Error(`all video interpolation servers are busy, try again later..`)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
state.load += 1
|
| 24 |
+
|
| 25 |
+
try {
|
| 26 |
+
const inputFilePath = path.join(tmpDir, fileName)
|
| 27 |
+
|
| 28 |
+
const instance = instances.shift()
|
| 29 |
+
instances.push(instance)
|
| 30 |
+
|
| 31 |
+
const api = await client(instance, {
|
| 32 |
+
hf_token: `${process.env.VC_HF_API_TOKEN}` as any
|
| 33 |
+
})
|
| 34 |
+
|
| 35 |
+
const video = await fs.readFile(inputFilePath)
|
| 36 |
+
|
| 37 |
+
const blob = new Blob([video], { type: 'video/mp4' })
|
| 38 |
+
// const blob = blobFrom(filePath)
|
| 39 |
+
const result = await api.predict(1, [
|
| 40 |
+
blob, // blob in 'parameter_5' Video component
|
| 41 |
+
steps, // number (numeric value between 1 and 4) in 'Interpolation Steps' Slider component
|
| 42 |
+
fps, // string (FALSE! it's a number) in 'FPS output' Radio component
|
| 43 |
+
])
|
| 44 |
+
|
| 45 |
+
const data = (result as any).data[0]
|
| 46 |
+
console.log('raw data:', data)
|
| 47 |
+
const { orig_name, data: remoteFilePath } = data
|
| 48 |
+
const remoteUrl = `${instance}/file=${remoteFilePath}`
|
| 49 |
+
console.log("remoteUrl:", remoteUrl)
|
| 50 |
+
await downloadFileToTmp(remoteUrl, fileName)
|
| 51 |
+
} catch (err) {
|
| 52 |
+
throw err
|
| 53 |
+
} finally {
|
| 54 |
+
state.load -= 1
|
| 55 |
+
}
|
| 56 |
}
|
src/production/renderStaticScene.mts
CHANGED
|
@@ -3,11 +3,14 @@ import path from "node:path"
|
|
| 3 |
import { v4 as uuidv4 } from "uuid"
|
| 4 |
import tmpDir from "temp-dir"
|
| 5 |
|
| 6 |
-
import { ImageSegment, RenderedScene, RenderRequest } from "../types.mts"
|
| 7 |
import { segmentImage } from "../utils/segmentImage.mts"
|
| 8 |
import { generateImageSDXLAsBase64 } from "../utils/generateImageSDXL.mts"
|
| 9 |
import { writeBase64ToFile } from "../utils/writeBase64ToFile.mts"
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
export async function renderStaticScene(scene: RenderRequest): Promise<RenderedScene> {
|
| 12 |
|
| 13 |
let imageBase64 = ""
|
|
|
|
| 3 |
import { v4 as uuidv4 } from "uuid"
|
| 4 |
import tmpDir from "temp-dir"
|
| 5 |
|
| 6 |
+
import { ImageSegment, RenderedScene, RenderingJob, RenderRequest } from "../types.mts"
|
| 7 |
import { segmentImage } from "../utils/segmentImage.mts"
|
| 8 |
import { generateImageSDXLAsBase64 } from "../utils/generateImageSDXL.mts"
|
| 9 |
import { writeBase64ToFile } from "../utils/writeBase64ToFile.mts"
|
| 10 |
|
| 11 |
+
|
| 12 |
+
const pendingJobs: RenderingJob[] = []
|
| 13 |
+
|
| 14 |
export async function renderStaticScene(scene: RenderRequest): Promise<RenderedScene> {
|
| 15 |
|
| 16 |
let imageBase64 = ""
|
src/types.mts
CHANGED
|
@@ -312,4 +312,17 @@ export interface RenderedScene {
|
|
| 312 |
error: string
|
| 313 |
maskBase64: string
|
| 314 |
segments: ImageSegment[]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
}
|
|
|
|
| 312 |
error: string
|
| 313 |
maskBase64: string
|
| 314 |
segments: ImageSegment[]
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// note: for video generation we are always going to have slow jobs,
|
| 318 |
+
// because we need multiple seconds, minutes, hours.. of video + audio
|
| 319 |
+
// but for rendering we aim at shorter delays, less than 45 seconds
|
| 320 |
+
// so the goal of rendering "jobs" is mostly to give the illusion that
|
| 321 |
+
// things go faster, by already providing some things like the background image,
|
| 322 |
+
// before we send
|
| 323 |
+
export interface RenderingJob {
|
| 324 |
+
scene: RenderRequest
|
| 325 |
+
result: RenderedScene
|
| 326 |
+
|
| 327 |
+
status: 'pending' | 'completed' | 'error'
|
| 328 |
}
|