jbilcke-hf HF staff commited on
Commit
6966d41
·
1 Parent(s): bd9ccc9
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);