微信聊天对话图自动化生成脚本

1,084 阅读4分钟

目录

  1. 1. 背景需求
  2. 2. 微信聊天框前端页面
  3. 3. 聊天内容的来源
  4. 4. 脚本实现思路
  5. 5. 聊天内容格式
  6. 6. 效果图

背景需求

最近刷到一些短视频,发现很多搞笑的短视频,内容为一张微信聊天的截图,视频时长大概10秒左右,于是就想尝试这种方式,能够通过自动化脚本来批量生成

微信聊天框前端页面

由于内容是微信聊天截图,所以需要一个前端微信聊天对话生成器,找到一个开源的项目,项目地址:github.com/Ele-Cat/vue…

提供了一个聊天框,能够自定义一些用户,模仿在微信中的对话,最终能够生成聊天截图。大概看了下页面效果,基本能够满足需求

效果图如下:

聊天效果图.png

聊天内容的来源

  • • 刚开始尝试用写段子的prompt去一些ai里面生成搞笑对话内容,但是感觉效果不好,毕竟中文的理解博大精深,想让它玩一些梗,目前还是比较困难
  • • 然后,准备了一个搞笑段子的示例内容,要求ai来模仿写出类似的,效果比上述直接去创造好一点,但是笑点有限,生成的多个内容里面,勉强有几个可用,但是也没法生成很惊艳的段子
  • • 然后,尝试了使用ocr识图,希望给它很多的已有聊天图片,去识别文字对话,然后进行仿写,但是尝试后,觉得一方面识别的准确度可能会有误;另一方面无法很好的区分对话内容来自于哪一个角色
  • • 所以,感觉问题还是存在,没法很精准的通过自动化,直接获取很搞笑的对话内容出来
  • • 后续也可以尝试用爬虫去一些搞笑段子网站,进行高质量的筛选爬取内容

脚本实现思路

这个项目属于开源项目,自己直接部署起来,然后进行二次改造,在代码里面加入自动化的过程,当然也是可行的,但是这种方式需要一定的基础,复杂度也不低

所以,尝试使用puppeteer来直接模拟浏览器,然后进行自动化操作,批量输入聊天内容,批量生成最终的微信聊天对话截图,只需要本地跑一个nodejs的简单项目即可

  1. 1. 通过puppeteer模拟打开浏览器,访问上述微信聊天对话生成器页面
  2. 2. 定位出页面的一些功能元素,比如各种按钮、输入框等
  3. 3. 将提前准备好的对话内容有序的输入到聊天框中
  4. 4. 最后生成聊天对话图片
  5. 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

什么才是真爱.png