Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 |
}
|