摘要:在这个快节奏的时代,长文阅读逐渐成为奢侈。本文将分享如何利用 DeepSeek(作为编剧)和 MiniMax(作为配音),在 Next.js 项目中构建一套自动化的播客生成系统,让你的技术博客也能“开口说话”。
为什么要做这个?
在一次锻炼的路上,打开微信文章,里面不仅可以看,还能听,这让我在路上边听,边锻炼,让我感觉十分舒服。
这让我萌生了一个想法:如果能把技术文章自动转换成双人对谈形式的播客,这样也不是很好吗?
不是枯燥的 TTS 朗读,而是像大佬面对面那样,有主持人引导、有博主解答的真实对话感。经过一番折腾,我基于 DeepSeek 和 MiniMax 实现了一套低成本、高质量的解决方案。
技术架构概览
我们的目标很简单:输入文章内容 → 输出 MP3 音频。但为了保证听感自然,中间需要经过精细的处理流程:
1.编剧阶段:提取文章核心,改编成口语化的双人逐字稿(DeepSeek)。 2.配音阶段:根据角色分配不同的音色,并发/串行合成语音(MiniMax)。 3.后期阶段:拼接音频片段,生成完整文件。
第一步:“AI 编剧” (DeepSeek)
要让播客听起来自然,剧本是灵魂。直接朗读文章会像听论文汇报,我们需要把书面语转化为口语。
这里我选择了 DeepSeek,它的中文理解能力极强,且 API 成本极低(性价比吊打 GPT-4)。
1. 定义剧本结构 (Schema)
使用 Vercel AI SDK 的 generateObject 配合 zod,我们可以强制模型输出严格的 JSON 格式,避免后续解析出错。
import { z } from 'zod';
// 定义严格的对话结构
const scriptSchema = z.object({
dialogue: z.array(
z.object({
speaker: z.enum(['Host', 'Blogger']), // 限定只有主持人和博主
text: z.string(),
})
),
});
2. Prompt 工程 这是最关键的一步。我们需要通过 System Prompt 赋予 AI “制作人”的人格。
export async function generateScript(content: string) {
// 截取前 1w 字符,防止 Token 溢出
const truncatedContent = content.slice(0, 10000);
const result = await generateObject({
model: deepseek('deepseek-chat'),
schema: scriptSchema,
system: `你是一个深受欢迎的技术播客制作人。请将这篇技术文章改编成一段 3 分钟左右的双人对谈脚本。
角色设定:
- Host (Jimmy): 好奇心强,代表初学者提问,语气轻松活泼。
- Blogger (小官): 技术博主本人,负责解答,语气专业但幽默。
核心要求:
1. **口语化!口语化!** 去掉所有书面转折词(如“然而”、“综上所述”)。
2. 开场白固定为:"欢迎收听 AI 码记,我是 Jimmy..."。
3. 避免尴尬的互夸,多一些基于内容的真实互动。`,
prompt: `文章内容:\n${truncatedContent}`,
});
return result.object.dialogue;
}
第二步:寻找“声优” (MiniMax)
有了剧本,接下来需要声音。MiniMax 的 speech-01 模型是目前中文 TTS 的第一梯队,语调起伏非常像真人。
为了区分角色,我选用了两款反差鲜明的音色:
- Host (Jimmy): female-shaonv (少女音,活泼)
- Blogger (小官): male-qn-qingse (青涩男音,沉稳)
语音合成核心代码
注意:MiniMax 返回的是音频 URL,我们需要在服务端将其下载为 Buffer 以便后续拼接。
const VOICE_MAP = {
Host: 'female-shaonv',
Blogger: 'male-qn-qingse',
};
export async function synthesizeSegment(text: string, speaker: 'Host' | 'Blogger'): Promise<Buffer> {
// ... API Key 配置 ...
const response = await fetch(`${MINIMAX_URL}?GroupId=${groupId}`, {
method: 'POST',
body: JSON.stringify({
text,
model: 'speech-01',
voice_id: VOICE_MAP[speaker],
audio_sample_rate: 32000, // 32k 采样率保证音质
vol: 1.0,
}),
// ... headers ...
});
const result = await response.json();
// 关键:获取 URL 后再次 fetch 拿到二进制数据
const audioResponse = await fetch(result.audio_file);
const arrayBuffer = await audioResponse.arrayBuffer();
return Buffer.from(arrayBuffer);
}
注:为什么需要拼接,由于我们需要在对话中生成2种不同声的对话,一个男声,及主持人女声,所以在一次性生成中没办法同时输出2种声音,所以我们需要把转换好的声音在重新拼接。
第三步:后期合成与流式传输
这一步就像导演剪辑,我们需要处理并发控制、音频拼接以及向前端的数据传输。
1. 并发控制与限流
TTS 接口通常有 QPS 限制。为了避免 429 Too Many Requests,我采用了一种简单的“串行+等待”策略:
const audioBuffers: Buffer[] = [];
for (let i = 0; i < script.length; i++) {
const line = script[i];
// 合成当前片段
const buffer = await synthesizeSegment(line.text, line.speaker);
audioBuffers.push(buffer);
// 简单的防限流策略:每句之间暂停一下
if (i < script.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
2. Buffer 拼接
Node.js 的 Buffer.concat 可以完美将多段音频无缝“胶水”粘合在一起。
const finalAudio = Buffer.concat(audioBuffers);
3. Server Action 的数据传输陷阱
这是最容易踩坑的地方! Next.js 的 Server Actions 不支持直接返回 Buffer 对象给客户端。我们需要将其转换为普通数组。
// ✅ 正确做法:转换为数字数组
const audioData = Array.from(finalAudio);
return { success: true, audioData };
4.前端如何还原 因为你传过去的是一个“数字数组”,浏览器不能直接播放它。所以在前端代码中,必须有逆向还原的一步:
// 前端接收到的 result.audioData 是一个巨大的数字数组 [12, 244, 11, ...]
// 1. 还原为二进制数组 (Uint8Array)
const buffer = new Uint8Array(result.audioData);
// 2. 封装为 Blob 对象,并告诉浏览器这是 'audio/mpeg' (即 MP3)
const blob = new Blob([buffer], { type: 'audio/mpeg' });
// 3. 生成可播放的 URL
const url = URL.createObjectURL(blob);
总结
- MiniMax API:生成了 MP3 数据。
- Buffer.concat:拼接了 MP3 数据。
- Array.from:打包 MP3 数据(为了过安检/网络传输)。
- 前端 new Blob:解包 MP3 数据(为了播放)。
现在,当用户访问我的博客文章时,只需点击顶部的「🎙️ 播客」按钮,随后就能得到一段娓娓道来的技术对谈。