Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
·
6966d41
1
Parent(s):
bd9ccc9
bump
Browse files
src/bug-in-bun/aitube_ffmpeg/analyze/extractFirstFrame.ts
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { promises as fs, existsSync } from 'node:fs';
|
2 |
+
import path from 'node:path';
|
3 |
+
import os from 'node:os';
|
4 |
+
|
5 |
+
import { v4 as uuidv4 } from 'uuid';
|
6 |
+
import ffmpeg from 'fluent-ffmpeg';
|
7 |
+
|
8 |
+
const validFormats = ['jpeg', 'png', 'webp'];
|
9 |
+
|
10 |
+
/**
|
11 |
+
* Extract the first frame from a video
|
12 |
+
*
|
13 |
+
* @param param0
|
14 |
+
* @returns
|
15 |
+
*/
|
16 |
+
export async function extractFirstFrame({
|
17 |
+
inputVideo,
|
18 |
+
outputFormat = 'jpeg'
|
19 |
+
}: {
|
20 |
+
inputVideo?: string
|
21 |
+
outputFormat?: "jpeg" | "png" | "webp"
|
22 |
+
|
23 |
+
}) {
|
24 |
+
if (!inputVideo) {
|
25 |
+
throw new Error(`inputVideo must be a file path or a base64 data-uri`);
|
26 |
+
}
|
27 |
+
|
28 |
+
// Validate output format
|
29 |
+
if (!validFormats.includes(outputFormat)) {
|
30 |
+
throw new Error(`Invalid output format. Choose one of: ${validFormats.join(', ')}`);
|
31 |
+
}
|
32 |
+
|
33 |
+
// Handle base64 input
|
34 |
+
let videoFilePath = inputVideo;
|
35 |
+
if (inputVideo.startsWith('data:')) {
|
36 |
+
const matches = inputVideo.match(/^data:video\/(\w+);base64,(.*)$/);
|
37 |
+
if (!matches) {
|
38 |
+
throw new Error('Invalid base64 input provided.');
|
39 |
+
}
|
40 |
+
const extension = matches[1];
|
41 |
+
const base64Content = matches[2];
|
42 |
+
|
43 |
+
videoFilePath = path.join(os.tmpdir(), `${uuidv4()}_inputVideo.${extension}`);
|
44 |
+
await fs.writeFile(videoFilePath, base64Content, 'base64');
|
45 |
+
} else if (!existsSync(videoFilePath)) {
|
46 |
+
throw new Error('Video file does not exist.');
|
47 |
+
}
|
48 |
+
|
49 |
+
// Create a temporary output file
|
50 |
+
const outputImagePath = path.join(os.tmpdir(), `${uuidv4()}.${outputFormat}`);
|
51 |
+
|
52 |
+
return new Promise((resolve, reject) => {
|
53 |
+
ffmpeg()
|
54 |
+
.input(videoFilePath)
|
55 |
+
.outputOptions([
|
56 |
+
'-vframes', '1', // Extract only one frame
|
57 |
+
'-f', 'image2', // Output format for the frame as image
|
58 |
+
'-an' // Disable audio
|
59 |
+
])
|
60 |
+
.output(outputImagePath)
|
61 |
+
.on('error', (err) => {
|
62 |
+
reject(new Error(`FFmpeg error: ${err.message}`));
|
63 |
+
})
|
64 |
+
.on('end', async () => {
|
65 |
+
try {
|
66 |
+
const imageBuffer = await fs.readFile(outputImagePath);
|
67 |
+
const imageBase64 = `data:image/${outputFormat};base64,${imageBuffer.toString('base64')}`;
|
68 |
+
resolve(imageBase64);
|
69 |
+
} catch (error) {
|
70 |
+
reject(new Error(`Error reading the image file: ${error}`));
|
71 |
+
} finally {
|
72 |
+
// Clean up temporary files
|
73 |
+
if (inputVideo.startsWith('data:')) {
|
74 |
+
await fs.unlink(videoFilePath);
|
75 |
+
}
|
76 |
+
await fs.unlink(outputImagePath);
|
77 |
+
}
|
78 |
+
})
|
79 |
+
.run();
|
80 |
+
});
|
81 |
+
}
|
src/bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64PngClient.ts
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { spawn } from 'child_process';
|
2 |
+
import path from 'node:path';
|
3 |
+
const __dirname = path.resolve();
|
4 |
+
|
5 |
+
export async function htmlToBase64PnClient(input) {
|
6 |
+
const jsonData = JSON.stringify(input)
|
7 |
+
const b64Data = Buffer.from(jsonData).toString('base64');
|
8 |
+
let stdoutData = '';
|
9 |
+
|
10 |
+
return await new Promise((resolve) => {
|
11 |
+
const proc = spawn('node', [
|
12 |
+
path.resolve(__dirname, 'htmlToBase64PngWorker.js'),
|
13 |
+
`--input-data${b64Data}`,
|
14 |
+
'--tagprocess'
|
15 |
+
], { shell: false });
|
16 |
+
|
17 |
+
proc.stdout.on('data', (data) => {
|
18 |
+
stdoutData += data;
|
19 |
+
});
|
20 |
+
|
21 |
+
proc.stderr.on('data', (data) => {
|
22 |
+
console.error(`NodeERR: ${data}`);
|
23 |
+
});
|
24 |
+
|
25 |
+
proc.on('exit', function () {
|
26 |
+
proc.kill();
|
27 |
+
resolve(JSON.parse(stdoutData));
|
28 |
+
});
|
29 |
+
});
|
30 |
+
}
|
src/bug-in-bun/aitube_ffmpeg/overlay/htmlToBase64PngWorker.js
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { promises as fs } from "node:fs"
|
2 |
+
import os from "node:os"
|
3 |
+
import path from "node:path"
|
4 |
+
|
5 |
+
import { v4 as uuidv4 } from "uuid"
|
6 |
+
import puppeteer from "puppeteer"
|
7 |
+
|
8 |
+
const inpDataB64 = process.argv.find((a) => a.startsWith('--input-data')).replace('--input-data', '')
|
9 |
+
const input = JSON.parse(Buffer.from(inpDataB64, 'base64').toString())
|
10 |
+
|
11 |
+
async function htmlToBase64PngWorker(input) {
|
12 |
+
|
13 |
+
let { outputImagePath, html, width, height } = input
|
14 |
+
|
15 |
+
// If no output path is provided, create a temporary file for output
|
16 |
+
if (!outputImagePath) {
|
17 |
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), uuidv4()))
|
18 |
+
outputImagePath = path.join(tempDir, `${uuidv4()}.png`)
|
19 |
+
}
|
20 |
+
|
21 |
+
const browser = await puppeteer.launch({
|
22 |
+
headless: true,
|
23 |
+
executablePath: os.type() === "Darwin"
|
24 |
+
? '/opt/homebrew/bin/chromium'
|
25 |
+
: '/usr/bin/chromium-browser',
|
26 |
+
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage']
|
27 |
+
});
|
28 |
+
|
29 |
+
const page = await browser.newPage()
|
30 |
+
|
31 |
+
page.setViewport({ width, height })
|
32 |
+
|
33 |
+
try {
|
34 |
+
await page.setContent(html)
|
35 |
+
|
36 |
+
const content = await page.$("body")
|
37 |
+
|
38 |
+
if (!content) { throw new Error (`Couldn't find body content`) }
|
39 |
+
|
40 |
+
const buffer = await content.screenshot({
|
41 |
+
path: outputImagePath,
|
42 |
+
omitBackground: true,
|
43 |
+
captureBeyondViewport: false,
|
44 |
+
type: "png",
|
45 |
+
})
|
46 |
+
|
47 |
+
const outputData = {
|
48 |
+
filePath: outputImagePath,
|
49 |
+
buffer
|
50 |
+
}
|
51 |
+
|
52 |
+
console.log(JSON.stringify(outputData))
|
53 |
+
} catch (err) {
|
54 |
+
console.error(err)
|
55 |
+
} finally {
|
56 |
+
await page.close()
|
57 |
+
await browser.close()
|
58 |
+
}
|
59 |
+
};
|
60 |
+
|
61 |
+
htmlToBase64PngWorker(input);
|