目录
- 1. 背景需求
- 2. 微信聊天框前端页面
- 3. 聊天内容的来源
- 4. 脚本实现思路
- 5. 聊天内容格式
- 6. 效果图
背景需求
最近刷到一些短视频,发现很多搞笑的短视频,内容为一张微信聊天的截图,视频时长大概10秒左右,于是就想尝试这种方式,能够通过自动化脚本来批量生成
微信聊天框前端页面
由于内容是微信聊天截图,所以需要一个前端微信聊天对话生成器,找到一个开源的项目,项目地址:github.com/Ele-Cat/vue…
提供了一个聊天框,能够自定义一些用户,模仿在微信中的对话,最终能够生成聊天截图。大概看了下页面效果,基本能够满足需求
效果图如下:
聊天内容的来源
- • 刚开始尝试用写段子的prompt去一些ai里面生成搞笑对话内容,但是感觉效果不好,毕竟中文的理解博大精深,想让它玩一些梗,目前还是比较困难
- • 然后,准备了一个搞笑段子的示例内容,要求ai来模仿写出类似的,效果比上述直接去创造好一点,但是笑点有限,生成的多个内容里面,勉强有几个可用,但是也没法生成很惊艳的段子
- • 然后,尝试了使用ocr识图,希望给它很多的已有聊天图片,去识别文字对话,然后进行仿写,但是尝试后,觉得一方面识别的准确度可能会有误;另一方面无法很好的区分对话内容来自于哪一个角色
- • 所以,感觉问题还是存在,没法很精准的通过自动化,直接获取很搞笑的对话内容出来
- • 后续也可以尝试用爬虫去一些搞笑段子网站,进行高质量的筛选爬取内容
脚本实现思路
这个项目属于开源项目,自己直接部署起来,然后进行二次改造,在代码里面加入自动化的过程,当然也是可行的,但是这种方式需要一定的基础,复杂度也不低
所以,尝试使用puppeteer来直接模拟浏览器,然后进行自动化操作,批量输入聊天内容,批量生成最终的微信聊天对话截图,只需要本地跑一个nodejs的简单项目即可
- 1. 通过puppeteer模拟打开浏览器,访问上述微信聊天对话生成器页面
- 2. 定位出页面的一些功能元素,比如各种按钮、输入框等
- 3. 将提前准备好的对话内容有序的输入到聊天框中
- 4. 最后生成聊天对话图片
- 5. 然后将图片,后台合成视频,加入背景音乐等操作
const puppeteer = require("puppeteer");
const fs = require("fs");
const ffmpegPath = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
async function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
async function parseAndProcessFile(path) {
const content = await readFile(path);
const lines = content.split("\n");
const data = [];
let currentBlock = [];
for (let line of lines) {
if (line.trim() === "") {
// this is an empty line
if (currentBlock.length > 0) {
data.push(currentBlock);
currentBlock = [];
}
} else {
const key = line[0];
const value = line.slice(1).trim();
if (key === "A" || key === "B") {
currentBlock.push({ key, value });
} else {
currentBlock.push({ title: line.slice(2).trim() });
}
}
}
// don't forget the last block
if (currentBlock.length > 0) {
data.push(currentBlock);
}
return data;
}
async function clickElement(page, xpathExpression, timeout = 30000) {
await page.waitForXPath(xpathExpression, { timeout });
const [element] = await page.$x(xpathExpression);
if (element) {
console.log("Clicking element:", xpathExpression);
await page.evaluate((el) => {
el.click();
}, element);
} else {
console.log("Element not found:", xpathExpression);
}
}
async function setInputValue(page, value) {
const xpathExpression = "//div[contains(@class, 'ant-card-body')]//textarea";
const [inputElement] = await page.$x(xpathExpression);
if (inputElement) {
await page.evaluate(
(el, val) => {
el.value = val;
el.dispatchEvent(new Event("input", { bubbles: true }));
},
inputElement,
value
);
}
}
async function switchUser(page, userIndex) {
const targetDivs = await page.$x(
'//div[contains(@class, "user-select-box")]//div[contains(@class, "user-item")]'
);
const userDiv = targetDivs[userIndex];
if (userDiv) {
await page.evaluate((el) => {
el.click();
}, userDiv);
}
}
function createVideoFromImage(options) {
const {
imagePath,
audioPath,
outputPath,
duration = 10, // 视频时长,默认10秒
audioBitrate = "256k", // 音频比特率,默认192k
imageBitrate = "300k", // 图片比特率,默认300k
} = options;
return new Promise((resolve, reject) => {
ffmpeg()
.setFfmpegPath(ffmpegPath)
.addInput(imagePath)
.addInput(audioPath)
.loop(duration) // 图片展示时长
.outputOptions(
"-c:v",
"libx264",
"-c:a",
"aac",
"-b:a",
audioBitrate,
"-shortest"
)
.save(outputPath)
.on("end", () => {
console.log("Video created successfully");
resolve();
})
.on("error", (err) => {
console.error("Error: " + err.message);
reject(err);
});
});
}
(async () => {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto("https://ele-cat.github.io/vue3-wechat-tool/");
await clickElement(page, "//button/span[contains(., '我已知晓,关闭')]");
await clickElement(page, "(//p[text()='对话设置'])");
parseAndProcessFile("xxx")
.then(async (data) => {
for (let block of data) {
//点击清空按钮,清空输入框
await clickElement(page, "//span[@class='anticon anticon-delete']");
await clickElement(page, "//span[text() = '确 定']");
let title;
for (let item of block) {
//角色
if (item.key === "A") {
await switchUser(page, 0);
// Do your processing for A here
await setInputValue(page, item.value.trim());
await clickElement(page, "//span[text()='发送']");
console.log("A says:", item.value.trim());
} else if (item.key === "B") {
// Do your processing for B here
await switchUser(page, 1);
await setInputValue(page, item.value.trim());
await clickElement(page, "//span[text()='发送']");
console.log("B says:", item.value.trim());
} else if (item.title !== undefined) {
// Do your processing for title here
title = item.title.trim();
}
}
// 每次执行完成,点击一次生成图片按钮
await clickElement(
page,
"//div[@class='wtc-button' and text()='生成图片']"
);
await page.waitForSelector(".ant-drawer-body > img");
const imgElements = await page.$x(
"//div[@class='ant-drawer-body']/img"
);
await page.waitForTimeout(1000);
//直接保存img的base64
let imgBase64;
while (!imgBase64) {
await page.waitForTimeout(1000);
imgBase64 = await page.evaluate(
(el) => el.getAttribute("src"),
imgElements[0]
);
}
let data64Data = imgBase64.replace(/^data:image/\w+;base64,/, "");
// 3. 然后将其解码:
let dataBuffer = Buffer.from(data64Data, "base64");
// 4. 最后保存为文件:
fs.writeFile(`F:\img\${title}.png`, dataBuffer, function (err) {
if (err) {
console.log(err);
} else {
console.log("保存成功");
}
});
//生成视频
createVideoFromImage({
imagePath: `F:\img\${title}.png`,
audioPath: "",
outputPath: `F:\img\${title}.mp4`,
duration: 10, // 设置视频时长为15秒
})
.then(() => {
console.log("Video creation complete");
})
.catch((err) => {
console.error("Video creation failed:", err);
});
}
})
.catch((err) => console.error(err));
await page.waitForTimeout(50000000);
await browser.close();
})();
聊天内容格式为:
标题什么才是真爱
A真爱就是你递给我一盒巧克力,我会很开心。
A真爱就是你把我当作唯一。
A真爱就是你愿意为我支付任何代价。
A真爱就是你愿意为我放弃世界。
A你说,你对我的真爱是什么?
B电量不足1%
说明:
- • 标题内容为截图、视频文件名
- • A和B开头为2个用户
效果图
什么才是真爱.png