HackerNews 播客生成器
HackerNews 播客生成器 - 核心技术实现
项目概述
这是一个基于 Next.js 15 的自动化播客生成系统,核心功能是:
- 抓取 HackerNews 热门资讯
- 使用 AI 将多条资讯整合成简短的双人播客对话(300字以内)
- 将对话文本转换为语音播客
技术栈:Next.js 15 (App Router) + TypeScript + TailwindCSS + 硅基流动 API
核心架构
项目采用三层架构:
1. API 层 (app/api/*) - 处理业务逻辑
2. 服务层 (lib/*) - 封装外部 API 调用
3. 展示层 (components/*) - React 组件
一、核心类型定义
文件:lib/types.ts
// HackerNews 故事类型
export interface HNStory {id: number;title: string;url?: string;text?: string;by: string;time: number;score: number;descendants?: number;
}// 播客类型
export interface Podcast {id: string;title: string;dialogue: string;audioUrl?: string | null;stories: HNStory[];createdAt: string;status: 'generating' | 'ready' | 'error' | 'dialogue_only';
}
二、HackerNews API 封装
文件:lib/hackernews.ts
import axios from 'axios';
import { HNStory } from './types';const HN_API_BASE = 'https://hacker-news.firebaseio.com/v0';/*** 获取热门故事 ID 列表*/
export async function getTopStoryIds(limit: number = 10): Promise<number[]> {const response = await axios.get(`${HN_API_BASE}/topstories.json`);return response.data.slice(0, limit);
}/*** 获取单个故事详情*/
export async function getStory(id: number): Promise<HNStory | null> {try {const response = await axios.get(`${HN_API_BASE}/item/${id}.json`);return response.data;} catch (error) {console.error(`Failed to fetch story ${id}:`, error);return null;}
}/*** 获取多个热门故事*/
export async function getTopStories(limit: number = 5): Promise<HNStory[]> {const ids = await getTopStoryIds(limit);const stories = await Promise.all(ids.map(id => getStory(id)));return stories.filter((story): story is HNStory => story !== null);
}/*** 格式化故事为文本摘要*/
export function formatStoryForPrompt(story: HNStory): string {return `标题: ${story.title}
作者: ${story.by}
评分: ${story.score}
${story.url ? `链接: ${story.url}` : ''}
${story.text ? `内容: ${story.text.substring(0, 200)}...` : ''}`;
}
核心要点:
- 使用 HackerNews 官方 API
- 并发获取多个故事详情(Promise.all)
- 格式化故事为 AI 可读的文本格式
三、硅基流动 API 封装
文件:lib/siliconflow.ts
3.1 生成播客对话(DeepSeek-V3)
import axios from 'axios';
import { HNStory } from './types';
import { formatStoryForPrompt } from './hackernews';const SILICONFLOW_API_BASE = 'https://api.siliconflow.cn/v1';/*** 使用 DeepSeek-V3 生成播客对谈内容*/
export async function generatePodcastDialogue(stories: HNStory[],apiKey: string
): Promise<string> {const storiesText = stories.map((story, index) => `${index + 1}. ${formatStoryForPrompt(story)}`).join('\n\n');const prompt = `你是一个专业的播客制作人。请根据以下 HackerNews 热门资讯,创作一段简短精炼的双人播客对谈内容。要求:
1. 使用 [S1] 和 [S2] 标记两位主持人的对话
2. S1 是一位技术专家,语气专业但不失幽默
3. S2 是一位好奇的提问者,善于提出有趣的问题
4. 将所有资讯整合成一个连贯的话题讨论,提炼核心趋势和亮点
5. 对话要自然流畅、通俗易懂,不要逐条罗列新闻
6. **总长度严格控制在 300 字以内**HackerNews 资讯:
${storiesText}请生成简短的播客对谈内容(300字以内):`;try {const response = await axios.post(`${SILICONFLOW_API_BASE}/chat/completions`,{model: 'deepseek-ai/DeepSeek-V3',messages: [{role: 'user',content: prompt}],max_tokens: 512,temperature: 0.7,top_p: 0.9},{headers: {'Authorization': `Bearer ${apiKey}`,'Content-Type': 'application/json'}});return response.data.choices[0].message.content;} catch (error) {if (axios.isAxiosError(error)) {const errorMessage = error.response?.data?.error?.message || error.response?.statusText || error.message;const statusCode = error.response?.status;throw new Error(`AI 对话生成失败 (${statusCode || 'Network Error'}): ${errorMessage}`);}throw new Error('AI 对话生成失败: 未知错误');}
}
核心要点:
- 使用 DeepSeek-V3 模型(推理速度快、质量高)
- Prompt 工程:明确角色定位、对话格式、长度限制
- 错误处理:提取详细的 API 错误信息
3.2 生成播客音频(MOSS-TTSD)
/*** 使用 MOSS-TTSD 生成播客音频* 返回 Base64 Data URL(适用于 Vercel 只读文件系统)*/
export async function generatePodcastAudio(dialogue: string,apiKey: string
): Promise<string> {try {const requestData = {model: 'fnlp/MOSS-TTSD-v0.5',input: dialogue,voice: 'fnlp/MOSS-TTSD-v0.5:alex',response_format: 'mp3',stream: false,speed: 1,gain: 0,max_tokens: 4096,};const response = await fetch(`${SILICONFLOW_API_BASE}/audio/speech`, {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': `Bearer ${apiKey}`,},body: JSON.stringify(requestData),});if (!response.ok) {const errorText = await response.text();throw new Error(`TTS 音频合成失败 (${response.status}): ${errorText}`);}const audioBuffer = await response.arrayBuffer();// 将音频转换为 Base64 Data URLconst base64Audio = Buffer.from(audioBuffer).toString('base64');const audioDataUrl = `data:audio/mpeg;base64,${base64Audio}`;return audioDataUrl;} catch (error) {if (error instanceof Error) {throw error;}throw new Error('TTS 音频合成失败: 未知错误');}
}
核心要点:
- 使用 MOSS-TTSD-v0.5 模型(支持中英文、语音克隆)
- 使用 fetch API 而非 axios(更好地处理二进制数据)
- 关键设计:返回 Base64 Data URL 而非文件路径
- 原因:Vercel 等平台是只读文件系统,无法写入文件
- 优势:无需文件存储,直接在浏览器中播放和下载
四、API 路由实现
4.1 获取资讯 API
文件:app/api/stories/route.ts
import { NextResponse } from 'next/server';
import { getTopStories } from '@/lib/hackernews';export async function GET(request: Request) {try {const { searchParams } = new URL(request.url);const limit = parseInt(searchParams.get('limit') || '5', 10);const stories = await getTopStories(limit);return NextResponse.json({success: true,stories});} catch (error) {console.error('Error fetching stories:', error);return NextResponse.json({ success: false, error: '获取资讯失败' },{ status: 500 });}
}
4.2 生成对话 API
文件:app/api/generate-dialogue/route.ts
import { NextResponse } from 'next/server';
import { generatePodcastDialogue } from '@/lib/siliconflow';
import { HNStory } from '@/lib/types';export async function POST(request: Request) {try {const { stories, apiKey } = await request.json();if (!apiKey) {return NextResponse.json({ success: false, error: '请提供 API Key' },{ status: 400 });}if (!stories || !Array.isArray(stories) || stories.length === 0) {return NextResponse.json({ success: false, error: '请提供有效的资讯列表' },{ status: 400 });}const dialogue = await generatePodcastDialogue(stories as HNStory[], apiKey);return NextResponse.json({success: true,dialogue});} catch (error) {console.error('Error generating dialogue:', error);return NextResponse.json({ success: false, error: '生成对谈内容失败' },{ status: 500 });}
}
4.3 生成音频 API
文件:app/api/generate-audio/route.ts
import { NextResponse } from 'next/server';export async function POST(request: Request) {try {const { dialogue, apiKey } = await request.json();if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) {return NextResponse.json({ success: false, error: '请提供有效的 API Key' },{ status: 400 });}if (!dialogue || typeof dialogue !== 'string' || dialogue.trim().length === 0) {return NextResponse.json({ success: false, error: '请提供对谈内容(对话文案不能为空)' },{ status: 400 });}// 调用 SiliconFlow TTS APIconst requestData = {model: 'fnlp/MOSS-TTSD-v0.5',input: dialogue,voice: 'fnlp/MOSS-TTSD-v0.5:alex',response_format: 'mp3',stream: false,speed: 1,gain: 0,max_tokens: 4096,};const response = await fetch('https://api.siliconflow.cn/v1/audio/speech', {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': `Bearer ${apiKey}`,},body: JSON.stringify(requestData),});if (!response.ok) {const errorText = await response.text();return NextResponse.json({ success: false, error: `TTS API 调用失败 (${response.status})`, details: errorText },{ status: response.status });}// 获取音频数据并转换为 Base64 Data URLconst audioBuffer = await response.arrayBuffer();const base64Audio = Buffer.from(audioBuffer).toString('base64');const audioDataUrl = `data:audio/mpeg;base64,${base64Audio}`;return NextResponse.json({success: true,audioUrl: audioDataUrl});} catch (error) {console.error('Error generating audio:', error);return NextResponse.json({ success: false, error: '生成音频失败',details: error instanceof Error ? error.message : String(error)},{ status: 500 });}
}
五、前端核心逻辑
文件:components/PodcastGenerator.tsx
5.1 状态管理
const [apiKey, setApiKey] = useState('');
const [storyLimit, setStoryLimit] = useState(5);
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState('');
const [podcast, setPodcast] = useState<Podcast | null>(null);
const [stories, setStories] = useState<HNStory[]>([]);
const [dialogue, setDialogue] = useState('');
5.2 API Key 持久化
const API_KEY_STORAGE_KEY = 'hackernews_podcast_api_key';// 从 localStorage 加载
useEffect(() => {const savedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY);if (savedApiKey) {setApiKey(savedApiKey);}
}, []);// 保存到 localStorage
const handleApiKeyChange = (value: string) => {setApiKey(value);if (value.trim()) {localStorage.setItem(API_KEY_STORAGE_KEY, value.trim());} else {localStorage.removeItem(API_KEY_STORAGE_KEY);}
};
5.3 一键生成播客流程
const handleGenerateAll = async () => {if (!apiKey.trim()) {setError('请输入硅基流动 API Key');return;}setLoading(true);setError('');try {// 步骤 1: 获取资讯setCurrentStep('📰 正在获取 HackerNews 资讯...');const storiesResponse = await fetch(`/api/stories?limit=${storyLimit}`);const storiesData = await storiesResponse.json();if (!storiesData.success) {throw new Error(storiesData.error || '获取资讯失败');}setStories(storiesData.stories);// 步骤 2: 生成对话文案setCurrentStep('💬 正在生成播客对话文案...');const dialogueResponse = await fetch('/api/generate-dialogue', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({apiKey: apiKey.trim(),stories: storiesData.stories,}),});const dialogueData = await dialogueResponse.json();if (!dialogueData.success) {throw new Error(dialogueData.error || '生成对话失败');}setDialogue(dialogueData.dialogue);// 步骤 3: 生成音频setCurrentStep('🎵 正在生成播客音频...');const audioResponse = await fetch('/api/generate-audio', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({apiKey: apiKey.trim(),dialogue: dialogueData.dialogue,}),});const audioData = await audioResponse.json();if (!audioData.success) {throw new Error(audioData.error || '生成音频失败');}// 创建播客对象const newPodcast: Podcast = {id: Date.now().toString(),title: `HackerNews 播客 - ${new Date().toLocaleDateString('zh-CN')}`,dialogue: dialogueData.dialogue,audioUrl: audioData.audioUrl,stories: storiesData.stories,createdAt: new Date().toISOString(),status: 'ready'};setPodcast(newPodcast);setCurrentStep('✅ 播客生成完成!');} catch (err) {setError('❌ ' + (err instanceof Error ? err.message : '未知错误'));} finally {setLoading(false);}
};
5.4 音频播放与下载
{/* 音频播放器 */}
<audio controls className="w-full" src={podcast.audioUrl}>您的浏览器不支持音频播放
</audio>{/* 下载按钮 */}
<button onClick={() => {const link = document.createElement('a');link.href = podcast.audioUrl;link.download = `hackernews-podcast-${Date.now()}.mp3`;document.body.appendChild(link);link.click();document.body.removeChild(link);
}}>下载音频
</button>
六、项目配置
6.1 依赖包
文件:package.json
{"dependencies": {"next": "15.1.6","react": "^19.0.0","react-dom": "^19.0.0","axios": "^1.7.9","lucide-react": "^0.469.0"},"devDependencies": {"@types/node": "^22","@types/react": "^19","@types/react-dom": "^19","typescript": "^5","tailwindcss": "^3.4.1","postcss": "^8.4.35","autoprefixer": "^10.4.17"}
}
6.2 环境变量
文件:.env.local.example
# 硅基流动 API Key
# 请在 https://cloud.siliconflow.cn/account/ak 获取
SILICONFLOW_API_KEY=your_api_key_here
七、关键技术要点总结
1. API 设计模式
- 三层架构:API 路由 → 服务层 → 外部 API
- 统一的错误处理和响应格式
- 参数验证和类型安全
2. Base64 Data URL 方案
- 问题:Vercel 等平台是只读文件系统
- 解决:将音频转换为 Base64 编码的 Data URL
- 优势:无需文件存储,直接在浏览器播放和下载
- 代价:文件大小增加约 33%
3. AI Prompt 工程
- 明确角色定位(S1 技术专家、S2 提问者)
- 严格的长度控制(300字以内)
- 整合式对话而非逐条罗列
4. 用户体验优化
- API Key 本地持久化(localStorage)
- 实时步骤提示(获取资讯 → 生成文案 → 合成音频)
- 错误信息详细展示
- 支持单独生成文案或完整播客
5. 性能优化
- 并发获取多个 HackerNews 故事(Promise.all)
- 使用 DeepSeek-V3(推理速度快)
- 控制对话长度减少音频生成时间
八、快速复现步骤
1. 创建 Next.js 项目
npx create-next-app@latest hackernews-podcast --typescript --tailwind --app
cd hackernews-podcast
2. 安装依赖
npm install axios lucide-react
3. 创建目录结构
mkdir -p lib components app/api/stories app/api/generate-dialogue app/api/generate-audio
4. 复制核心代码
按照本文档的代码片段,依次创建:
lib/types.ts
- 类型定义lib/hackernews.ts
- HackerNews API 封装lib/siliconflow.ts
- 硅基流动 API 封装app/api/stories/route.ts
- 获取资讯 APIapp/api/generate-dialogue/route.ts
- 生成对话 APIapp/api/generate-audio/route.ts
- 生成音频 APIcomponents/PodcastGenerator.tsx
- 主组件app/page.tsx
- 首页
5. 配置环境变量
cp .env.local.example .env.local
# 编辑 .env.local,填入硅基流动 API Key
6. 启动开发服务器
npm run dev
访问 http://localhost:3000 即可使用!
九、API 接口说明
硅基流动 API
Chat Completions(对话生成)
POST https://api.siliconflow.cn/v1/chat/completions
Authorization: Bearer {API_KEY}{"model": "deepseek-ai/DeepSeek-V3","messages": [{"role": "user", "content": "..."}],"max_tokens": 512,"temperature": 0.7
}
Audio Speech(语音合成)
POST https://api.siliconflow.cn/v1/audio/speech
Authorization: Bearer {API_KEY}{"model": "fnlp/MOSS-TTSD-v0.5","input": "对话文本","voice": "fnlp/MOSS-TTSD-v0.5:alex","response_format": "mp3"
}
HackerNews API
GET https://hacker-news.firebaseio.com/v0/topstories.json
GET https://hacker-news.firebaseio.com/v0/item/{id}.json
十、总结
这个项目展示了如何将多个 API 整合成一个完整的应用:
- 数据获取:HackerNews API
- 内容生成:DeepSeek-V3 AI 模型
- 语音合成:MOSS-TTSD TTS 模型
核心创新点:
- Base64 Data URL 方案解决无服务器环境的文件存储问题
- Prompt 工程实现多条资讯的智能整合
- 流式用户体验设计(步骤提示、错误处理)
通过本文档,您可以完整复现整个项目!🎉