使用Node.js构建Fish-Speech 1.5的REST API服务
1. 引言
Fish-Speech 1.5作为当前最先进的开源文本转语音模型,凭借其出色的多语言支持和高质量的语音合成能力,在开发者社区中广受好评。虽然官方提供了WebUI界面,但在实际生产环境中,我们往往需要通过API方式集成TTS功能到自己的应用中。
本文将带你一步步使用Node.js为Fish-Speech 1.5构建一个高性能的REST API服务。无论你是想要为你的聊天机器人添加语音功能,还是需要为内容创作平台集成文本转语音服务,这个方案都能满足你的需求。我们将重点介绍异步处理、负载均衡等关键技术,确保你的API服务既稳定又高效。
2. 环境准备与项目搭建
2.1 Node.js环境配置
首先确保你的系统已经安装了Node.js 18.0或更高版本。建议使用nvm(Node Version Manager)来管理多个Node.js版本:
# 安装nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash # 安装Node.js 18 nvm install 18 nvm use 182.2 创建项目并安装依赖
创建一个新的项目目录并初始化:
mkdir fish-speech-api cd fish-speech-api npm init -y安装必要的依赖包:
# 核心框架依赖 npm install express cors helmet morgan npm install axios form-data multer # 开发依赖 npm install -D nodemon typescript @types/node @types/express npm install -D @types/cors @types/multer # 可选:进程管理和负载均衡 npm install pm22.3 项目结构设计
建议采用以下目录结构来组织你的代码:
fish-speech-api/ ├── src/ │ ├── controllers/ # 路由控制器 │ ├── services/ # 业务逻辑层 │ ├── utils/ # 工具函数 │ ├── middleware/ # 自定义中间件 │ ├── types/ # TypeScript类型定义 │ └── app.ts # 应用入口 ├── config/ # 配置文件 ├── logs/ # 日志文件 └── package.json3. 核心API服务实现
3.1 基础Express服务器搭建
创建一个基础的Express服务器,包含必要的中间件和安全配置:
// src/app.ts import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; import { config } from './config'; import { ttsRouter } from './controllers/ttsController'; const app = express(); // 安全中间件 app.use(helmet()); app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); // 日志中间件 app.use(morgan('combined')); // 路由注册 app.use('/api/tts', ttsRouter); // 健康检查端点 app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); }); // 启动服务器 const PORT = config.port || 3000; app.listen(PORT, () => { console.log(`Fish-Speech API服务运行在端口 ${PORT}`); });3.2 Fish-Speech服务集成
创建与Fish-Speech WebUI服务交互的核心服务类:
// src/services/fishSpeechService.ts import axios from 'axios'; import FormData from 'form-data'; import { config } from '../config'; export class FishSpeechService { private baseURL: string; constructor() { this.baseURL = `http://${config.fishSpeechHost}:${config.fishSpeechPort}`; } async generateSpeech(text: string, referenceAudio?: Buffer): Promise<Buffer> { try { const formData = new FormData(); formData.append('text', text); if (referenceAudio) { formData.append('reference_audio', referenceAudio, { filename: 'reference.wav', contentType: 'audio/wav' }); } const response = await axios.post( `${this.baseURL}/generate`, formData, { headers: formData.getHeaders(), responseType: 'arraybuffer', timeout: 120000 // 2分钟超时 } ); return Buffer.from(response.data); } catch (error) { console.error('语音生成失败:', error); throw new Error(`语音生成服务错误: ${error.message}`); } } async getAvailableVoices(): Promise<any[]> { try { const response = await axios.get(`${this.baseURL}/voices`); return response.data; } catch (error) { console.error('获取可用音色失败:', error); return []; } } }3.3 异步任务处理机制
对于长时间的语音生成任务,实现异步处理机制:
// src/services/queueService.ts import { Worker, Queue, Job } from 'bullmq'; import IORedis from 'ioredis'; import { FishSpeechService } from './fishSpeechService'; const connection = new IORedis({ host: config.redisHost, port: config.redisPort }); export const ttsQueue = new Queue('tts-generation', { connection }); export class QueueService { private fishSpeechService: FishSpeechService; constructor() { this.fishSpeechService = new FishSpeechService(); this.setupWorker(); } private setupWorker(): void { new Worker('tts-generation', async (job: Job) => { const { text, referenceAudio } = job.data; return await this.fishSpeechService.generateSpeech(text, referenceAudio); }, { connection }); } async addTTSTask(text: string, referenceAudio?: Buffer): Promise<string> { const job = await ttsQueue.add('generate-speech', { text, referenceAudio }); return job.id; } async getJobStatus(jobId: string): Promise<any> { const job = await ttsQueue.getJob(jobId); if (!job) { return { status: 'not_found' }; } const state = await job.getState(); return { status: state, result: job.returnvalue, progress: job.progress }; } }4. REST API控制器实现
4.1 文本转语音端点
创建主要的TTS生成端点:
// src/controllers/ttsController.ts import express from 'express'; import multer from 'multer'; import { QueueService } from '../services/queueService'; import { FishSpeechService } from '../services/fishSpeechService'; const router = express.Router(); const upload = multer({ storage: multer.memoryStorage() }); const queueService = new QueueService(); const fishSpeechService = new FishSpeechService(); // 同步生成语音 router.post('/generate', upload.single('reference_audio'), async (req, res) => { try { const { text } = req.body; const referenceAudio = req.file?.buffer; if (!text) { return res.status(400).json({ error: '缺少文本参数' }); } const audioBuffer = await fishSpeechService.generateSpeech(text, referenceAudio); res.set({ 'Content-Type': 'audio/wav', 'Content-Length': audioBuffer.length }); res.send(audioBuffer); } catch (error) { console.error('语音生成错误:', error); res.status(500).json({ error: '语音生成失败' }); } }); // 异步生成语音 router.post('/generate-async', upload.single('reference_audio'), async (req, res) => { try { const { text } = req.body; const referenceAudio = req.file?.buffer; if (!text) { return res.status(400).json({ error: '缺少文本参数' }); } const jobId = await queueService.addTTSTask(text, referenceAudio); res.json({ jobId, statusUrl: `/api/tts/job/${jobId}/status` }); } catch (error) { console.error('创建异步任务错误:', error); res.status(500).json({ error: '创建任务失败' }); } }); // 获取任务状态 router.get('/job/:jobId/status', async (req, res) => { try { const { jobId } = req.params; const status = await queueService.getJobStatus(jobId); res.json(status); } catch (error) { console.error('获取任务状态错误:', error); res.status(500).json({ error: '获取状态失败' }); } }); // 获取可用音色 router.get('/voices', async (req, res) => { try { const voices = await fishSpeechService.getAvailableVoices(); res.json(voices); } catch (error) { console.error('获取音色列表错误:', error); res.status(500).json({ error: '获取音色失败' }); } }); export { router as ttsRouter };4.2 请求验证中间件
添加输入验证中间件确保数据安全:
// src/middleware/validation.ts import { Request, Response, NextFunction } from 'express'; export const validateTTSParams = (req: Request, res: Response, next: NextFunction): void => { const { text } = req.body; if (!text || typeof text !== 'string') { res.status(400).json({ error: '文本参数必须为非空字符串' }); return; } if (text.length > 1000) { res.status(400).json({ error: '文本长度不能超过1000字符' }); return; } next(); }; export const validateAudioFile = (req: Request, res: Response, next: NextFunction): void => { if (req.file) { const allowedTypes = ['audio/wav', 'audio/mpeg', 'audio/ogg']; if (!allowedTypes.includes(req.file.mimetype)) { res.status(400).json({ error: '不支持音频格式,请使用WAV、MP3或OGG格式' }); return; } if (req.file.size > 10 * 1024 * 1024) { // 10MB限制 res.status(400).json({ error: '音频文件大小不能超过10MB' }); return; } } next(); };5. 性能优化与部署
5.1 负载均衡配置
使用PM2实现多进程负载均衡:
// ecosystem.config.js module.exports = { apps: [{ name: 'fish-speech-api', script: './dist/app.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, env_production: { NODE_ENV: 'production', PORT: 3000 }, max_memory_restart: '1G', watch: false, merge_logs: true, error_file: './logs/err.log', out_file: './logs/out.log', log_file: './logs/combined.log' }] };启动服务:
npm run build pm2 start ecosystem.config.js --env production5.2 Redis缓存优化
添加结果缓存减少重复生成:
// src/services/cacheService.ts import IORedis from 'ioredis'; export class CacheService { private redis: IORedis; constructor() { this.redis = new IORedis({ host: config.redisHost, port: config.redisPort }); } async getCachedAudio(text: string, voiceId?: string): Promise<Buffer | null> { const key = this.generateCacheKey(text, voiceId); const cached = await this.redis.getBuffer(key); return cached; } async cacheAudio(text: string, audio: Buffer, voiceId?: string, ttl: number = 3600): Promise<void> { const key = this.generateCacheKey(text, voiceId); await this.redis.setex(key, ttl, audio); } private generateCacheKey(text: string, voiceId?: string): string { const normalizedText = text.trim().toLowerCase(); return voiceId ? `tts:${voiceId}:${normalizedText}` : `tts:${normalizedText}`; } }5.3 配置文件管理
创建统一的配置文件:
// src/config/index.ts export const config = { port: process.env.PORT || 3000, fishSpeechHost: process.env.FISH_SPEECH_HOST || 'localhost', fishSpeechPort: process.env.FISH_SPEECH_PORT || 6006, redisHost: process.env.REDIS_HOST || 'localhost', redisPort: parseInt(process.env.REDIS_PORT || '6379'), maxTextLength: parseInt(process.env.MAX_TEXT_LENGTH || '1000'), rateLimit: { windowMs: 15 * 60 * 1000, // 15分钟 max: 100 // 每窗口最大请求数 } };6. 总结
通过本文的指导,你应该已经成功构建了一个基于Node.js的Fish-Speech 1.5 REST API服务。这个服务不仅提供了同步和异步的语音生成接口,还包含了负载均衡、缓存优化等生产环境需要的功能。
实际使用中,这个API服务可以轻松集成到各种应用中,比如为内容管理系统添加语音朗读功能,或者为聊天机器人提供语音交互能力。异步处理机制确保了即使生成长文本时也不会阻塞请求,而Redis缓存则显著提升了重复请求的响应速度。
部署到生产环境时,记得根据实际流量调整PM2的实例数量,并配置合适的监控和告警机制。对于更高可用性的需求,可以考虑使用Docker容器化部署,或者结合Kubernetes进行集群管理。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。