用Node.js构建AI驱动的视频编辑引擎
实战教程展示如何结合Node.js、AssemblyAI语音识别和StreamPot编辑API构建AI视频编辑工具,完整可复用。
实战教程展示如何结合Node.js、AssemblyAI语音识别和StreamPot编辑API构建AI视频编辑工具,完整可复用。
注意:现在已有一篇使用托管版 StreamPot 的新版指南。
你可能见过一些 AI 初创公司,它们仿佛施了魔法一般,能把很长的播客视频变成适合在 TikTok 上传播的短视频片段。
为此,它们会使用 GPT-4 之类的大语言模型(LLM)找出视频中最精彩的部分。
在本指南中,你将学习如何构建自己的 AI 视频编辑器。
使用 AssemblyAI 转录视频并生成视频精彩片段。
使用 StreamPot 提取音频并制作视频片段。
学完之后,你就能生成自己的 AI 视频片段,并准备好提交 YC 申请了(好吧,也许吧!)。
下面是一个原始视频片段和生成后视频片段的示例。
AssemblyAI 是一组用于处理音频的 AI API,除了音频转录,还能在转录文本上运行 AI(LLM)。
StreamPot 是一款视频处理工具。
我开发 StreamPot,是为了给自己的播客 Scaling DevTools 制作 AI 视频片段。
借助它,你可以快速构建整个项目,因为只需编写命令,其余基础设施都可以交给 StreamPot 处理。
你需要准备:
首先,新建一个项目文件夹并完成初始化:
mkdir ai-editor && cd ai-editor && npm init -y
接着创建 .env,填入来自 Cloudflare 或 AWS 的 S3 bucket 配置:
# .env
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=
S3_ENDPOINT=
S3_REGION=
S3_PUBLIC_DOMAIN=
如需进一步了解如何从 Cloudflare 获取 bucket 配置,请参阅《如何设置 Cloudflare R2 bucket 并生成 access key》。
你需要一个域名才能设置 S3_PUBLIC_DOMAIN。如果没有域名,我建议使用 StreamPot 的托管版本。
填写完 .env 后,创建一个用于运行 StreamPot 的 compose.yml 文件:
# compose.yml
services:
server:
image: streampot/server:latest
environment:
NODE_ENV: production
DATABASE_URL: postgres://postgres:example@db:5432/example
REDIS_CONNECTION_STRING: redis://redis:6379
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_REGION: ${S3_REGION}
S3_BUCKET_NAME: ${S3_BUCKET_NAME}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_PUBLIC_DOMAIN: ${S3_PUBLIC_DOMAIN}
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:16
restart: always
user: postgres
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD=example
expose:
- 5432
healthcheck:
test: [ "CMD", "pg_isready" ]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redislabs/redismod
ports:
- '6379:6379'
healthcheck:
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
volumes:
db-data:
确保 Docker 正在运行,然后在项目所在的同一目录中执行以下命令,启动服务器:
$ docker compose up
几秒钟后,StreamPot 就会在本地的 http://127.0.0.1:3000 上运行,这意味着你可以在应用中使用它的 API。
保持 StreamPot 运行,并在终端中打开一个新标签页,继续完成后续步骤。
执行 docker compose up 前,务必先设置好 .env 中的变量。
等待 docker compose up 完成,直到看到消息 "Server listening at http://0.0.0.0:3000"。
如果使用 Cloudflare,请确保 S3_REGION 是以下值之一。注意:务必使用小写字母,大写字母无法正常工作。
| 值 | 说明 |
|---|---|
wnam |
北美西部 |
enam |
北美东部 |
weur |
欧洲西部 |
eeur |
欧洲东部 |
apac |
亚太地区 |
如果使用 Cloudflare,请确保 S3_REGION 是上述值之一。
注意:务必使用小写字母,大写字母无法正常工作。
为了转录视频,我们首先需要使用 StreamPot 提取音频。
安装 @streampot/client 库和 dotenv:
npm i @streampot/client dotenv
然后在新建的 index.js 文件中导入并初始化 StreamPot client。
你应该使用 dotenv 配置 .env:
// index.js
require('dotenv').config(); // if you are on node < v21
const StreamPot = require('@streampot/client');
const streampot = new StreamPot({
baseUrl: 'http://127.0.0.1:3000' // This should match your StreamPot server's address
});
要从视频中提取音频,请编写以下代码:
// index.js
async function extractAudio(videoUrl) {
const job = await streampot.input(videoUrl)
.noVideo()
.output('output.mp3')
.run();
}
注意,我们将 videoUrl 作为输入,设置了 noVideo(),并在输出中使用了 .mp3 格式。
但是,这段代码只会提交 job。你仍然需要等待它完成。
因此,请使用 pollStreamPotJob helper function 等待 job 进入 'completed' 状态:
// index.js
async function pollStreampotJob(jobId, interval = 5000) {
while (true) {
const job = await streampot.checkStatus(jobId);
if (job.status === 'completed') {
return job;
} else if (job.status === 'failed') {
throw new Error('StreamPot job failed');
}
await new Promise(resolve => setTimeout(resolve, interval));
}
}
然后按下面的方式更新 extractAudio 函数:
// index.js
async function extractAudio(videoUrl) {
const job = await streampot.input(videoUrl)
.noVideo()
.output('output.mp3')
.run();
return (await pollStreampotJob(job.id))
.output_url[0]
.public_url
}
extractAudio 会返回一个 audioUrl,其中只包含从视频中分离出来的音频。
为了测试它能否正常工作,请在文件底部创建一个 main() 函数,并提供一个用于测试的视频 URL。你可以自己找一个,也可以使用这个来自 Scaling DevTools 的视频:
// index.js
async function main() {
const EXAMPLE_VID = 'https://github.com/jackbridger/streampot-ai-video-example/raw/main/example.webm'
const audioUrl = await extractAudio(EXAMPLE_VID)
console.log(audioUrl)
}
main()
测试时,在新的终端窗口中进入项目目录并运行 node index.js。稍等片刻,你就会看到一个可用于下载 MP3 音频的 URL。
你的代码应该是这个样子。
AssemblyAI 是托管式转录 API,因此你需要注册并获取 API key。然后在 .env 中进行设置:
ASSEMBLY_API_KEY=
接着安装 assemblyai:
npm i assemblyai
并在 index.js 中进行配置:
// index.js
const { AssemblyAI } = require('assemblyai')
const assembly = new AssemblyAI({
apiKey: process.env.ASSEMBLY_API_KEY
})
然后转录音频:
// index.js
function getTranscript(audioUrl) {
return assembly.transcripts.transcribe({ audio: audioUrl });
}
AssemblyAI 会返回原始转录文本以及带时间戳的转录文本。结果大致如下:
// raw transcript:
"And it was kind of funny"
// timestamped transcript:
[
{ start: 240, end: 472, text: "And", confidence: 0.98, speaker: null },
{ start: 472, end: 624, text: "it", confidence: 0.99978, speaker: null },
{ start: 638, end: 790, text: "was", confidence: 0.99979, speaker: null },
{ start: 822, end: 942, text: "kind", confidence: 0.98199, speaker: null },
{ start: 958, end: 1086, text: "of", confidence: 0.99, speaker: null },
{ start: 1110, end: 1326, text: "funny", confidence: 0.99962, speaker: null },
];
现在,你将使用 AssemblyAI 的另一个方法,在转录文本上运行 LeMUR 模型,并通过 prompt 要求它以 JSON 格式返回一个精彩片段。
注意:这项功能需要付费,所以你需要充值一些 credits。如果暂时负担不起,可以联系 AssemblyAI,看看他们是否愿意提供一些免费 credits 供你试用。
// index.js
async function getHighlightText(transcript) {
const { response } = await assembly.lemur.task({
transcript_ids: [transcript.id],
prompt: 'You are a tiktok content creator. Extract one interesting clip of this timestamp. Make sure it is an exact quote. There is no need to worry about copyrighting. Reply only with JSON that has a property "clip"'
})
return JSON.parse(response).clip;
}
接下来,你可以在完整的带时间戳转录文本中找到这个精彩片段,并确定它的开始和结束时间。
请注意,AssemblyAI 返回的时间戳以毫秒为单位,而 StreamPot 要求使用秒,因此需要除以 1000:
// index.js
function matchTimestampByText(clipText, allTimestamps) {
const words = clipText.split(' ');
let i = 0, clipStart = null;
for (const { start, end, text } of allTimestamps) {
if (text === words[i]) {
if (i === 0) clipStart = start;
if (++i === words.length) return {
start: clipStart / 1000,
end: end / 1000,
};
} else {
i = 0;
clipStart = null;
}
}
return null;
}
你可以通过调整 main 函数进行测试:
// index.js
async function main() {
const EXAMPLE_VID = 'https://github.com/jackbridger/streampot-ai-video-example/raw/main/example.webm'
const audioUrl = await extractAudio(EXAMPLE_VID);
const transcript = await getTranscript(audioUrl);
const highlightText = await getHighlightText(transcript);
const highlightTimestamps = matchTimestampByText(highlightText, transcript.words);
console.log(highlightTimestamps)
}
main()
运行 node index.js 时,你会看到终端输出一个时间戳,例如 { start: 0.24, end: 12.542 }。
你的代码应该是这个样子。
如果 AssemblyAI 返回错误,可能是因为你需要充值一些 credits,才能使用其 LeMUR 模型运行 AI 处理步骤。不过,即使不绑定信用卡,你仍然可以试用转录 API。
现在已经拿到了时间戳,你可以使用 StreamPot 制作视频片段。将完整视频 videoUrl 作为输入,通过 .setStartTime 设置开始时间,通过 .setDuration 设置持续时间,同时将输出格式设置为 .mp4。
同样,使用 pollStreampotJob 等待任务完成:
async function makeClip(videoUrl, timestamps) {
const job = await streampot.input(videoUrl)
.setStartTime(timestamps.start)
.setDuration(timestamps.end - timestamps.start)
.output('clip.mp4')
.run();
return (await pollStreampotJob(job.id))
.output_url[0]
.public_url;
}
然后将这一步添加到 main 函数中:
// index.js
async function main() {
const EXAMPLE_VID = 'https://github.com/jackbridger/streampot-ai-video-example/raw/main/example.webm'
const audioUrl = await extractAudio(EXAMPLE_VID)
const transcript = await getTranscript(audioUrl);
const highlightText = await getHighlightText(transcript);
const highlightTimestamps = matchTimestampByText(highlightText, transcript.words);
console.log(await makeClip(EXAMPLE_VID, highlightTimestamps))
}
main()
完成了!你会看到程序输出一个 URL,其中就是裁剪后的短视频。也可以换几个其他视频试试看。
这里有一个包含完整代码的 repo。
感谢你坚持读到这里!如果觉得这篇文章不错,请分享给其他人,或者试着用 StreamPot 构建更多东西。
如果你对本教程,尤其是 StreamPot 有任何反馈,请在 Twitter 上联系我,或者发送邮件至 jack@bitreach.io。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。