feat: ChatTTS对话式语音合成服务初始版本

This commit is contained in:
2026-04-22 16:21:01 +08:00
commit f7c1bed80e
4 changed files with 480 additions and 0 deletions

147
README.md Normal file
View File

@@ -0,0 +1,147 @@
# ChatTTS 语音合成服务
专为对话场景设计的 TTS 模型服务,支持情感控制。
## 端口
- **服务端口**: 12002
## 功能
- 对话式语音合成(有语气变化)
- 情感控制(开心、悲伤、笑声)
- 批量合成
- 中文效果好
## 硬件要求
- **GPU**: 推荐 NVIDIA GPU显存 ≥ 4GB
- **CPU**: 可运行但较慢
- **内存**: ≥ 16GB
## 部署步骤
### 1. 安装依赖
```bash
pip install -r requirements.txt
```
### 2. 启动服务
```bash
# 默认端口 12002
python3 server.py
# 或使用脚本
PORT=12002 ./start.sh
```
首次启动会自动下载 ChatTTS 模型(约 2GB
### 3. 验证服务
```bash
curl http://localhost:12002/
```
返回:
```json
{
"status": "ok",
"service": "ChatTTS",
"model_loaded": true,
"device": "cuda"
}
```
## API 接口
### 合成语音
```bash
POST /synthesize
Content-Type: multipart/form-data
参数:
- text: 要合成的文本
- temperature: 温度参数(默认 0.3
- top_p: Top-P 参数(默认 0.7
- top_k: Top-K 参数(默认 20
返回:
{
"audio_url": "/audio/xxx.wav",
"duration": 3.5,
"text": "你好",
"timestamp": "..."
}
```
**示例**
```bash
curl -X POST http://localhost:12002/synthesize \
-F "text=你好,很高兴见到你"
```
### 带情感合成
```bash
POST /synthesize/emotion
参数:
- text: 文本
- emotion: 情感类型neutral/happy/sad/laugh
返回: 同上
```
**情感类型**
- `neutral`: 平静
- `happy`: 开心
- `sad`: 悲伤
- `laugh`: 笑声
### 批量合成
```bash
POST /synthesize/batch
Content-Type: application/json
Body:
[
{"text": "第一句"},
{"text": "第二句"}
]
```
### 获取音频文件
```bash
GET /audio/{filename}
```
## 特点对比
| 特点 | Edge TTS | ChatTTS |
|------|----------|---------|
| 部署 | 无需部署 | 需 GPU |
| 质量 | 高 | 对话优化 |
| 情感 | 固定音色 | 可控 |
| 延迟 | 1-3秒 | 0.5-1秒 |
| 离线 | ❌ | ✅ |
## 与语音对话网页集成
在 voice-chat-web 的 TTS 设置中选择 ChatTTS服务地址配置为
```
http://192.168.2.5:12002
```
## 注意事项
1. 模型首次加载需要下载约 2GB
2. GPU 运行速度更快CPU 可用但较慢
3. 长文本建议分段合成
4. 情感标记功能需要 ChatTTS 版本支持

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
fastapi==0.110.0
uvicorn==0.27.1
python-multipart==0.0.9
torch==2.2.0
torchaudio==2.2.0
transformers==4.38.0
ChatTTS==0.1.1

315
server.py Normal file
View File

@@ -0,0 +1,315 @@
"""
ChatTTS 语音合成服务
专为对话场景设计的 TTS 模型
"""
import os
import io
import uuid
import logging
import tempfile
import wave
import numpy as np
from typing import Optional
from datetime import datetime
import torch
import torchaudio
from fastapi import FastAPI, UploadFile, File, HTTPException, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
# 配置
PORT = int(os.getenv("PORT", "12002"))
SAMPLE_RATE = 24000 # ChatTTS 默认采样率
AUDIO_DIR = os.getenv("AUDIO_DIR", "audio_output")
os.makedirs(AUDIO_DIR, exist_ok=True)
# 日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="ChatTTS Service",
description="对话式语音合成服务",
version="1.0.0"
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ChatTTS 模型(延迟加载)
chat_model = None
class SynthesizeRequest(BaseModel):
"""合成请求"""
text: str
temperature: float = 0.3
top_p: float = 0.7
top_k: int = 20
class SynthesizeResponse(BaseModel):
"""合成响应"""
audio_url: str
duration: float
text: str
timestamp: str
def load_model():
"""加载 ChatTTS 模型"""
global chat_model
if chat_model is None:
logger.info("Loading ChatTTS model...")
try:
import ChatTTS
chat_model = ChatTTS.Chat()
# 加载模型
# 方式一:从本地加载(如果有)
# 方式二:自动下载
chat_model.load(
compile=True, # 编译优化
device="cuda" if torch.cuda.is_available() else "cpu"
)
logger.info("ChatTTS model loaded successfully")
except Exception as e:
logger.error(f"Failed to load ChatTTS: {e}")
raise
return chat_model
def save_audio(audio_tensor: torch.Tensor, filename: str) -> str:
"""
保存音频文件
audio_tensor: shape [1, samples] 或 [samples]
"""
filepath = os.path.join(AUDIO_DIR, filename)
# 确保 tensor 正确形状
if audio_tensor.dim() == 1:
audio_tensor = audio_tensor.unsqueeze(0)
# 保存为 WAV
torchaudio.save(filepath, audio_tensor, SAMPLE_RATE, format="wav")
return filepath
@app.on_event("startup")
async def startup():
"""启动时预加载模型"""
try:
load_model()
logger.info(f"ChatTTS service ready on port {PORT}")
except Exception as e:
logger.warning(f"Model load delayed: {e}")
@app.get("/")
async def root():
"""健康检查"""
return {
"status": "ok",
"service": "ChatTTS",
"model_loaded": chat_model is not None,
"device": "cuda" if torch.cuda.is_available() else "cpu"
}
@app.get("/health")
async def health():
"""健康检查"""
model_loaded = chat_model is not None
return {
"status": "ok" if model_loaded else "loading",
"gpu": torch.cuda.is_available()
}
@app.post("/synthesize", response_model=SynthesizeResponse)
async def synthesize(
text: str = Form(..., description="要合成的文本"),
temperature: float = Form(0.3, description="温度参数"),
top_p: float = Form(0.7, description="Top-P参数"),
top_k: int = Form(20, description="Top-K参数")
):
"""
合成语音
ChatTTS 特点:
- 支持对话场景,有语气变化
- 支持笑声、叹气等情感
- 中文效果好
"""
try:
model = load_model()
# 生成唯一文件名
filename = f"{uuid.uuid4().hex}.wav"
# 合成参数
params = {
'temperature': temperature,
'top_p': top_p,
'top_k': top_k,
'spk_emb': None, # 可选:说话人嵌入
}
# 合成语音
logger.info(f"Synthesizing: {text[:50]}...")
# ChatTTS 生成
audio_tensor = model.infer(
[text],
params=params
)[0] # 返回是列表,取第一个
# 保存音频
filepath = save_audio(audio_tensor, filename)
# 计算时长
duration = audio_tensor.shape[-1] / SAMPLE_RATE
audio_url = f"/audio/{filename}"
logger.info(f"Generated audio: {duration:.2f}s")
return SynthesizeResponse(
audio_url=audio_url,
duration=round(duration, 2),
text=text,
timestamp=datetime.now().isoformat()
)
except Exception as e:
logger.error(f"Synthesis error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.post("/synthesize/batch")
async def synthesize_batch(requests: list[SynthesizeRequest]):
"""
批量合成语音
"""
try:
model = load_model()
texts = [r.text for r in requests]
# 统一参数
params = {
'temperature': requests[0].temperature,
'top_p': requests[0].top_p,
'top_k': requests[0].top_k,
}
# 批量生成
audio_tensors = model.infer(texts, params=params)
results = []
for i, audio_tensor in enumerate(audio_tensors):
filename = f"{uuid.uuid4().hex}.wav"
filepath = save_audio(audio_tensor, filename)
duration = audio_tensor.shape[-1] / SAMPLE_RATE
results.append({
"audio_url": f"/audio/{filename}",
"duration": round(duration, 2),
"text": texts[i]
})
return {"results": results}
except Exception as e:
logger.error(f"Batch synthesis error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/audio/{filename}")
async def get_audio(filename: str):
"""获取音频文件"""
filepath = os.path.join(AUDIO_DIR, filename)
if os.path.exists(filepath):
return FileResponse(filepath, media_type="audio/wav")
raise HTTPException(status_code=404, detail="Audio not found")
@app.delete("/audio/{filename}")
async def delete_audio(filename: str):
"""删除音频文件"""
filepath = os.path.join(AUDIO_DIR, filename)
if os.path.exists(filepath):
os.remove(filepath)
return {"status": "deleted"}
return {"status": "not_found"}
# 情感控制接口ChatTTS 特色)
@app.post("/synthesize/emotion")
async def synthesize_with_emotion(
text: str = Form(...),
emotion: str = Form("neutral", description="情感neutral/happy/sad/laugh")
):
"""
带情感的语音合成
ChatTTS 支持情感控制:
- neutral: 平静
- happy: 开心
- sad: 悲伤
- laugh: 笑声
"""
try:
model = load_model()
# 根据情感调整参数
emotion_params = {
'neutral': {'temperature': 0.3, 'top_p': 0.7},
'happy': {'temperature': 0.5, 'top_p': 0.8},
'sad': {'temperature': 0.2, 'top_p': 0.6},
'laugh': {'temperature': 0.6, 'top_p': 0.9},
}
params = emotion_params.get(emotion, emotion_params['neutral'])
# 添加情感标记ChatTTS 特有)
if emotion == 'laugh':
# 添加笑声标记
text = f"[laugh]{text}"
elif emotion == 'happy':
text = f"[happy]{text}"
elif emotion == 'sad':
text = f"[sad]{text}"
filename = f"{uuid.uuid4().hex}.wav"
audio_tensor = model.infer([text], params=params)[0]
filepath = save_audio(audio_tensor, filename)
duration = audio_tensor.shape[-1] / SAMPLE_RATE
return SynthesizeResponse(
audio_url=f"/audio/{filename}",
duration=round(duration, 2),
text=text,
timestamp=datetime.now().isoformat()
)
except Exception as e:
logger.error(f"Emotion synthesis error: {e}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=PORT)

11
start.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# ChatTTS 服务启动脚本
cd "$(dirname "$0")"
PORT=${PORT:-12002}
echo "Starting ChatTTS service on port $PORT..."
echo "Device: $(python3 -c 'import torch; print("cuda" if torch.cuda.is_available() else "cpu")')"
python3 server.py