feat: 升级项目适配火山引擎TTS v3 API
新增: - 添加.env.example环境变量配置示例 - 重构鉴权逻辑,支持新旧两种鉴权模式 - 适配v3版API接口,实现流式响应 - 完善README文档和配置说明 移除: - 删除原Linux启动脚本start_linux.sh 优化: - 重构代码结构,适配新的API参数格式 - 优化日志输出和错误处理
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# ByteDance TTS v3 API 全兼容配置示例
|
||||
# 将此文件复制为 .env 并填入实际配置
|
||||
|
||||
# ==========================================
|
||||
# 必需的环境变量
|
||||
# ==========================================
|
||||
|
||||
# 鉴权模式(必要参数)
|
||||
# - legacy: 旧版鉴权,使用 AppID + AccessKey(v1资产升级推荐)
|
||||
# - new: 新版鉴权,使用 ApiKey(新控制台用户推荐)
|
||||
BYTEDANCE_TTS_AUTH_MODE=legacy
|
||||
|
||||
# 资源信息ID(决定使用1.0还是2.0模型)
|
||||
# 语音合成模型:
|
||||
# - seed-tts-1.0: 豆包语音合成模型1.0字符版
|
||||
# - seed-tts-1.0-concurr: 豆包语音合成模型1.0并发版
|
||||
# - seed-tts-2.0: 豆包语音合成模型2.0字符版
|
||||
# 声音复刻模型:
|
||||
# - seed-icl-1.0: 声音复刻1.0字符版
|
||||
# - seed-icl-1.0-concurr: 声音复刻1.0并发版
|
||||
# - seed-icl-2.0: 声音复刻2.0字符版
|
||||
BYTEDANCE_TTS_RESOURCE_ID=seed-tts-1.0
|
||||
|
||||
# 发音人(音色)ID,具体参考火山引擎音色列表
|
||||
# 注意:1.0音色只能搭配 seed-tts-1.0 Resource ID
|
||||
# 2.0音色只能搭配 seed-tts-2.0 Resource ID
|
||||
BYTEDANCE_TTS_SPEAKER=your_speaker_id_here
|
||||
|
||||
# ==========================================
|
||||
# 鉴权参数(根据 AUTH_MODE 选择配置)
|
||||
# ==========================================
|
||||
|
||||
# ---------- legacy 模式需要配置 ----------
|
||||
# 火山引擎旧版控制台获取的 AppID
|
||||
BYTEDANCE_TTS_APP_ID=your_app_id_here
|
||||
|
||||
# 火山引擎旧版控制台获取的 Access Key
|
||||
BYTEDANCE_TTS_ACCESS_KEY=your_access_key_here
|
||||
|
||||
# ---------- new 模式需要配置 ----------
|
||||
# 火山引擎新版控制台获取的 API Key
|
||||
BYTEDANCE_TTS_API_KEY=your_api_key_here
|
||||
|
||||
# ==========================================
|
||||
# 可选的环境变量
|
||||
# ==========================================
|
||||
|
||||
# 请求超时时间,默认30秒
|
||||
BYTEDANCE_TTS_TIMEOUT=30s
|
||||
|
||||
# OpenAI兼容接口的API密钥(可选)
|
||||
# 配置后,客户端请求需要携带 Authorization: Bearer <OPENAI_TTS_API_KEY>
|
||||
OPENAI_TTS_API_KEY=your_openai_compatible_key_here
|
||||
|
||||
# 服务监听端口,默认8080
|
||||
PORT=8080
|
||||
@@ -1,139 +1,232 @@
|
||||
# ByteDance TTS to OpenAI API Adapter - Linux部署指南
|
||||
# 字节跳动火山引擎TTS v3 API 转 OpenAI 兼容接口
|
||||
|
||||
## 快速开始
|
||||
## 项目简介
|
||||
|
||||
这是为Linux环境优化的TTS服务器版本,可以将字节跳动TTS服务适配为OpenAI TTS API格式。
|
||||
本项目将字节跳动火山引擎TTS(文本转语音)v3 API封装为OpenAI兼容的TTS API接口,使原本调用OpenAI TTS服务的应用可以无缝切换到火山引擎TTS服务。
|
||||
|
||||
### 主要特性
|
||||
|
||||
- ✅ 完全兼容OpenAI `/v1/audio/speech` API接口
|
||||
- ✅ 支持火山引擎TTS v3 API(单向流式)
|
||||
- ✅ 支持两种鉴权模式:Legacy(AppID+AccessKey) 和 New(API Key)
|
||||
- ✅ 支持多种发音人和模型版本
|
||||
- ✅ 内置速率限制和统计功能
|
||||
- ✅ 支持配置API密钥验证
|
||||
- ✅ 跨平台支持(Windows/Linux/macOS)
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `tts_server_linux.go` - Linux优化版主程序
|
||||
- `start_linux.sh` - Linux启动脚本
|
||||
- `fix_crlf.sh` - 换行符修复工具
|
||||
- `go.mod` 和 `go.sum` - Go模块依赖文件
|
||||
- `tts_server.go` - 主程序源码
|
||||
- `.env.example` - 环境变量配置示例
|
||||
- `go.mod` / `go.sum` - Go模块依赖
|
||||
|
||||
## 部署步骤
|
||||
## 快速开始
|
||||
|
||||
### 1. 上传文件到Linux服务器
|
||||
### 前置要求
|
||||
|
||||
将本文件夹中的所有文件上传到Linux服务器,建议放在`/www/wwwroot/tts-server/`目录。
|
||||
- Go 1.19 或更高版本
|
||||
- 火山引擎账号并开通TTS服务
|
||||
|
||||
### 2. 修复换行符(重要!)
|
||||
|
||||
在Linux服务器上,执行以下命令修复可能存在的Windows CRLF换行符问题:
|
||||
### 1. 编译程序
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/tts-server
|
||||
chmod +x fix_crlf.sh
|
||||
./fix_crlf.sh
|
||||
go build -o tts_server tts_server.go
|
||||
```
|
||||
|
||||
### 3. 安装Go环境(如果未安装)
|
||||
### 2. 配置环境变量
|
||||
|
||||
复制 `.env.example` 为 `.env` 并填入你的配置:
|
||||
|
||||
```bash
|
||||
# 下载Go 1.21.5
|
||||
cd /tmp
|
||||
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
|
||||
|
||||
# 解压到/usr/local
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
|
||||
|
||||
# 设置环境变量
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
|
||||
echo 'export GOPROXY=https://goproxy.cn,direct' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# 验证安装
|
||||
go version
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 4. 启动服务
|
||||
编辑 `.env` 文件,填入必要的配置参数。
|
||||
|
||||
### 3. 启动服务
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/tts-server
|
||||
chmod +x start_linux.sh
|
||||
./start_linux.sh start
|
||||
# Windows
|
||||
tts_server.exe
|
||||
|
||||
# Linux/macOS
|
||||
./tts_server
|
||||
```
|
||||
|
||||
## 服务管理命令
|
||||
|
||||
- 启动服务:`./start_linux.sh start`
|
||||
- 停止服务:`./start_linux.sh stop`
|
||||
- 重启服务:`./start_linux.sh restart`
|
||||
- 查看状态:`./start_linux.sh status`
|
||||
- 查看日志:`./start_linux.sh logs`
|
||||
|
||||
## 接口使用
|
||||
|
||||
- OpenAI TTS API兼容端点:`http://你的服务器IP:8080/v1/audio/speech`
|
||||
- 健康检查:`http://你的服务器IP:8080/health`
|
||||
|
||||
## API调用示例
|
||||
|
||||
```bash
|
||||
curl -X POST "http://你的服务器IP:8080/v1/audio/speech" \
|
||||
-H "Authorization: Bearer sk-7IBXpzK1YszwGArEMvLGzSdZe93rXVxg4CFBe5KRlqs4dVJO" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"tts-1","input":"你好,这是一个测试文本","voice":"alloy","speed":1.0}' \
|
||||
-o output.wav
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 端口被占用
|
||||
|
||||
如果8080端口被占用,可以通过环境变量修改端口:
|
||||
|
||||
```bash
|
||||
export PORT=8081 && ./start_linux.sh start
|
||||
```
|
||||
|
||||
### 启动失败
|
||||
|
||||
查看日志文件获取详细信息:
|
||||
|
||||
```bash
|
||||
cat /www/wwwroot/tts-server/logs/tts-server.log
|
||||
```
|
||||
服务默认监听 `8080` 端口。
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
服务现在支持通过环境变量配置所有参数,推荐使用这种方式而不是直接修改代码。
|
||||
### 必需参数
|
||||
|
||||
### 必须的环境变量
|
||||
| 变量名 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| `BYTEDANCE_TTS_AUTH_MODE` | 鉴权模式:`legacy` 或 `new` | `legacy` |
|
||||
| `BYTEDANCE_TTS_RESOURCE_ID` | 资源ID,决定模型版本 | `seed-tts-1.0` |
|
||||
| `BYTEDANCE_TTS_SPEAKER` | 发音人(音色)ID | `zh_female_qingxin` |
|
||||
|
||||
- `BYTEDANCE_TTS_APP_ID` - 字节跳动TTS应用ID
|
||||
- `BYTEDANCE_TTS_BEARER_TOKEN` - 字节跳动TTS访问令牌
|
||||
- `BYTEDANCE_TTS_CLUSTER` - 字节跳动TTS业务集群
|
||||
- `BYTEDANCE_TTS_VOICE_TYPE` - 字节跳动TTS声音类型
|
||||
### 鉴权参数(二选一)
|
||||
|
||||
### 可选的环境变量
|
||||
**Legacy 模式(旧版控制台):**
|
||||
- `BYTEDANCE_TTS_APP_ID` - 应用ID
|
||||
- `BYTEDANCE_TTS_ACCESS_KEY` - 访问密钥
|
||||
|
||||
- `BYTEDANCE_TTS_ENDPOINT` - 字节跳动TTS服务端点(默认:https://openspeech.bytedance.com/api/v1/tts)
|
||||
- `BYTEDANCE_TTS_TIMEOUT` - 字节跳动TTS请求超时时间(默认:30s,支持Go duration格式,如"60s"、"1m")
|
||||
- `OPENAI_TTS_API_KEY` - OpenAI TTS API密钥(未设置时允许任意key)
|
||||
- `PORT` - 服务监听端口(默认:8080)
|
||||
**New 模式(新版控制台):**
|
||||
- `BYTEDANCE_TTS_API_KEY` - API密钥
|
||||
|
||||
### 环境变量使用示例
|
||||
### 可选参数
|
||||
|
||||
```bash
|
||||
# 启动服务时设置环境变量
|
||||
export BYTEDANCE_TTS_APP_ID=your_app_id
|
||||
export BYTEDANCE_TTS_BEARER_TOKEN=your_token
|
||||
export BYTEDANCE_TTS_CLUSTER=your_cluster
|
||||
export BYTEDANCE_TTS_VOICE_TYPE=your_voice_type
|
||||
export OPENAI_TTS_API_KEY=sk-your_api_key
|
||||
export PORT=8081
|
||||
./start_linux.sh start
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `BYTEDANCE_TTS_TIMEOUT` | 请求超时时间 | `30s` |
|
||||
| `OPENAI_TTS_API_KEY` | OpenAI兼容接口的API密钥(逗号分隔支持多个) | 无 |
|
||||
| `PORT` | 服务监听端口 | `8080` |
|
||||
|
||||
### Resource ID 说明
|
||||
|
||||
| Resource ID | 模型说明 |
|
||||
|-------------|----------|
|
||||
| `seed-tts-1.0` | 豆包语音合成模型1.0字符版 |
|
||||
| `seed-tts-1.0-concurr` | 豆包语音合成模型1.0并发版 |
|
||||
| `seed-tts-2.0` | 豆包语音合成模型2.0字符版 |
|
||||
| `seed-icl-1.0` | 声音复刻1.0字符版 |
|
||||
| `seed-icl-1.0-concurr` | 声音复刻1.0并发版 |
|
||||
| `seed-icl-2.0` | 声音复刻2.0字符版 |
|
||||
|
||||
**注意:** 1.0音色只能搭配 `seed-tts-1.0` Resource ID,2.0音色只能搭配 `seed-tts-2.0` Resource ID。
|
||||
|
||||
## API 使用说明
|
||||
|
||||
### OpenAI 兼容接口
|
||||
|
||||
**端点:** `POST /v1/audio/speech`
|
||||
|
||||
**请求头:**
|
||||
- `Content-Type: application/json`
|
||||
- `Authorization: Bearer <你的API密钥>`(如果配置了OPENAI_TTS_API_KEY)
|
||||
|
||||
**请求体:**
|
||||
```json
|
||||
{
|
||||
"model": "tts-1",
|
||||
"input": "你好,这是一个测试文本",
|
||||
"voice": "alloy",
|
||||
"response_format": "wav",
|
||||
"speed": 1.0
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
**参数说明:**
|
||||
- `model` - 模型名称(OpenAI兼容,实际不影响)
|
||||
- `input` - 要合成的文本
|
||||
- `voice` - 发音人(OpenAI兼容,实际不影响)
|
||||
- `response_format` - 输出格式:`wav`、`mp3`、`opus`、`aac`、`flac`
|
||||
- `speed` - 语速:0.25 ~ 4.0
|
||||
|
||||
1. 确保Go版本至少为1.19
|
||||
2. 确保服务器有足够的网络权限访问字节跳动TTS服务
|
||||
3. 必须设置所有必需的环境变量,否则服务会使用默认值并显示警告信息
|
||||
4. 可以通过设置环境变量而不是直接修改代码来配置服务
|
||||
**示例调用:**
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/v1/audio/speech" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"tts-1","input":"你好,世界","voice":"alloy","speed":1.0}' \
|
||||
-o output.wav
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### 统计信息
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/stats
|
||||
```
|
||||
|
||||
## 支持的发音人
|
||||
|
||||
具体发音人列表请参考火山引擎官方文档:
|
||||
- 1.0音色:https://www.volcengine.com/docs/6561/97454
|
||||
- 2.0音色:https://www.volcengine.com/docs/6561/1340515
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 如何获取鉴权信息?
|
||||
|
||||
- 登录火山引擎控制台
|
||||
- 进入"语音合成"服务
|
||||
- 在应用管理中创建应用并获取AppID和AccessKey(Legacy模式)
|
||||
- 或在新版控制台获取API Key(New模式)
|
||||
|
||||
### 2. 端口被占用怎么办?
|
||||
|
||||
通过环境变量修改端口:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
set PORT=8081 && tts_server.exe
|
||||
|
||||
# Linux/macOS
|
||||
PORT=8081 ./tts_server
|
||||
```
|
||||
|
||||
### 3. 如何配置多个API密钥?
|
||||
|
||||
使用逗号分隔:
|
||||
|
||||
```bash
|
||||
OPENAI_TTS_API_KEY=sk-key1,sk-key2,sk-key3
|
||||
```
|
||||
|
||||
### 4. 查看日志
|
||||
|
||||
服务启动后会输出详细日志,包括:
|
||||
- 服务启动信息
|
||||
- 鉴权模式和配置状态
|
||||
- 请求统计信息
|
||||
- 错误详情
|
||||
|
||||
## 部署建议
|
||||
|
||||
### Linux Systemd 服务
|
||||
|
||||
创建 `/etc/systemd/system/tts-server.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ByteDance TTS to OpenAI API Adapter
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/www/wwwroot/tts-server
|
||||
EnvironmentFile=/www/wwwroot/tts-server/.env
|
||||
ExecStart=/www/wwwroot/tts-server/tts_server
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动服务:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable tts-server
|
||||
sudo systemctl start tts-server
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用非商业用途许可协议。您可以免费使用本软件用于非商业目的,但禁止用于任何商业活动。详细条款请参阅项目中的 [LICENSE](LICENSE) 文件。
|
||||
本项目采用非商业用途许可协议。您可以免费使用本软件用于非商业目的,但禁止用于任何商业活动。详细条款请参阅 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请检查:
|
||||
1. 环境变量配置是否正确
|
||||
2. 网络是否能访问火山引擎TTS服务
|
||||
3. 鉴权信息是否有效
|
||||
4. Resource ID与Speaker是否匹配
|
||||
|
||||
-292
@@ -1,292 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ByteDance TTS to OpenAI API Adapter 启动脚本 (Linux优化版)
|
||||
# 使用Unix LF换行符,避免Windows CRLF问题
|
||||
|
||||
# 设置Go代理(中国大陆用户推荐)
|
||||
export GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
# 项目目录 - 自动获取当前目录
|
||||
PROJECT_DIR=$(pwd)
|
||||
BINARY_NAME="tts-server"
|
||||
MAIN_FILE="tts_server.go"
|
||||
PID_FILE="$PROJECT_DIR/tts-server.pid"
|
||||
LOG_FILE="$PROJECT_DIR/logs/tts-server.log"
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "$PROJECT_DIR/logs"
|
||||
|
||||
# 检查Go环境
|
||||
check_go() {
|
||||
if ! command -v go &> /dev/null; then
|
||||
echo "❌ Go 未安装或未添加到 PATH"
|
||||
echo "请先安装 Go 语言环境"
|
||||
return 1
|
||||
fi
|
||||
echo "✅ Go 环境检查通过: $(go version)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_deps() {
|
||||
echo "正在检查 Go 模块依赖..."
|
||||
cd $PROJECT_DIR
|
||||
if [ ! -f go.mod ]; then
|
||||
echo "❌ 未找到 go.mod 文件"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 下载依赖
|
||||
go mod download
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Go 依赖下载失败"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ Go 依赖检查完成"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 函数:启动服务
|
||||
start() {
|
||||
echo "🚀 正在启动 TTS API 服务器..."
|
||||
|
||||
# 检查Go环境
|
||||
if ! check_go; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查是否已经运行
|
||||
if [ -f $PID_FILE ]; then
|
||||
PID=$(cat $PID_FILE)
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "⚠️ 服务已经在运行中 (PID: $PID)"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 切换到项目目录
|
||||
cd $PROJECT_DIR
|
||||
|
||||
# 检查主程序文件
|
||||
if [ ! -f $MAIN_FILE ]; then
|
||||
echo "❌ 未找到主程序文件: $MAIN_FILE"
|
||||
echo "请确保 $MAIN_FILE 文件存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查依赖
|
||||
if ! check_deps; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 构建项目
|
||||
echo "🔨 正在构建项目..."
|
||||
go build -o $BINARY_NAME $MAIN_FILE
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ 构建失败"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ 构建成功"
|
||||
|
||||
# 启动服务
|
||||
echo "🌟 正在启动服务..."
|
||||
nohup ./$BINARY_NAME > $LOG_FILE 2>&1 &
|
||||
PID=$!
|
||||
echo $PID > $PID_FILE
|
||||
|
||||
# 等待启动
|
||||
sleep 2
|
||||
|
||||
# 检查启动状态
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "✅ TTS API 服务器启动成功 (PID: $PID)"
|
||||
echo "📍 端口: 8080"
|
||||
echo "📄 日志文件: $LOG_FILE"
|
||||
echo "🌐 访问地址: http://你的服务器IP:8080"
|
||||
echo "💚 健康检查: http://你的服务器IP:8080/health"
|
||||
return 0
|
||||
else
|
||||
echo "❌ 服务启动失败,请检查日志: $LOG_FILE"
|
||||
rm -f $PID_FILE
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:停止服务
|
||||
stop() {
|
||||
echo "🛑 正在停止 TTS API 服务器..."
|
||||
|
||||
if [ -f $PID_FILE ]; then
|
||||
PID=$(cat $PID_FILE)
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
# 优雅关闭(发送 SIGTERM)
|
||||
kill -TERM $PID
|
||||
|
||||
# 等待进程结束
|
||||
for i in {1..10}; do
|
||||
if ! ps -p $PID > /dev/null 2>&1; then
|
||||
echo "✅ 服务已优雅停止 (PID: $PID)"
|
||||
rm -f $PID_FILE
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 强制杀死进程
|
||||
echo "⚠️ 进程未响应,强制终止..."
|
||||
kill -KILL $PID
|
||||
echo "✅ 服务已强制停止 (PID: $PID)"
|
||||
rm -f $PID_FILE
|
||||
else
|
||||
echo "⚠️ 服务未运行"
|
||||
rm -f $PID_FILE
|
||||
fi
|
||||
else
|
||||
echo "⚠️ PID文件不存在,服务可能未运行"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:重启服务
|
||||
restart() {
|
||||
echo "🔄 正在重启 TTS API 服务器..."
|
||||
stop
|
||||
sleep 2
|
||||
start
|
||||
}
|
||||
|
||||
# 函数:查看状态
|
||||
status() {
|
||||
echo "📊 TTS API 服务器状态:"
|
||||
echo "================================"
|
||||
|
||||
if [ -f $PID_FILE ]; then
|
||||
PID=$(cat $PID_FILE)
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "🟢 状态: 运行中"
|
||||
echo "🆔 PID: $PID"
|
||||
echo "🕐 运行时间: $(ps -o etime= -p $PID | tr -d ' ')"
|
||||
echo "💾 内存使用: $(ps -o rss= -p $PID | tr -d ' ') KB"
|
||||
echo "🌐 端口: 8080"
|
||||
echo "📄 日志: $LOG_FILE"
|
||||
|
||||
# 检查端口是否监听
|
||||
if command -v netstat &> /dev/null; then
|
||||
if netstat -tlnp 2>/dev/null | grep ":8080" | grep "$PID" > /dev/null; then
|
||||
echo "🔗 端口监听: ✅"
|
||||
else
|
||||
echo "🔗 端口监听: ❌"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "🔴 状态: 未运行(PID文件存在但进程不存在)"
|
||||
rm -f $PID_FILE
|
||||
fi
|
||||
else
|
||||
echo "🔴 状态: 未运行"
|
||||
fi
|
||||
|
||||
echo "================================"
|
||||
}
|
||||
|
||||
# 函数:查看日志
|
||||
logs() {
|
||||
if [ -f $LOG_FILE ]; then
|
||||
echo "📄 实时日志 (按 Ctrl+C 退出):"
|
||||
echo "================================"
|
||||
tail -f $LOG_FILE
|
||||
else
|
||||
echo "❌ 日志文件不存在: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:查看最近日志
|
||||
lastlog() {
|
||||
if [ -f $LOG_FILE ]; then
|
||||
echo "📄 最近 50 行日志:"
|
||||
echo "================================"
|
||||
tail -n 50 $LOG_FILE
|
||||
else
|
||||
echo "❌ 日志文件不存在: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# 函数:测试服务
|
||||
test() {
|
||||
echo "🧪 正在测试 TTS API 服务器..."
|
||||
|
||||
# 检查健康状态
|
||||
echo "1. 健康检查测试..."
|
||||
if command -v curl &> /dev/null; then
|
||||
response=$(curl -s -w "%{http_code}" -o /tmp/health_check.tmp http://localhost:8080/health)
|
||||
if [ "$response" = "200" ]; then
|
||||
echo "✅ 健康检查通过"
|
||||
cat /tmp/health_check.tmp | python3 -m json.tool 2>/dev/null || cat /tmp/health_check.tmp
|
||||
else
|
||||
echo "❌ 健康检查失败 (HTTP: $response)"
|
||||
fi
|
||||
rm -f /tmp/health_check.tmp
|
||||
else
|
||||
echo "⚠️ curl 未安装,跳过健康检查"
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
usage() {
|
||||
echo "ByteDance TTS to OpenAI API Adapter 管理脚本 (Linux优化版)"
|
||||
echo ""
|
||||
echo "使用方法: $0 {命令}"
|
||||
echo ""
|
||||
echo "可用命令:"
|
||||
echo " start - 启动服务"
|
||||
echo " stop - 停止服务"
|
||||
echo " restart - 重启服务"
|
||||
echo " status - 查看详细状态"
|
||||
echo " logs - 实时查看日志"
|
||||
echo " lastlog - 查看最近日志"
|
||||
echo " test - 测试服务"
|
||||
echo " help - 显示帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " ./start_linux.sh start"
|
||||
echo " ./start_linux.sh status"
|
||||
echo " ./start_linux.sh logs"
|
||||
}
|
||||
|
||||
# 主程序逻辑
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
logs)
|
||||
logs
|
||||
;;
|
||||
lastlog)
|
||||
lastlog
|
||||
;;
|
||||
test)
|
||||
test
|
||||
;;
|
||||
help|--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "❌ 未知命令: $1"
|
||||
echo ""
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit $?
|
||||
+182
-91
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
@@ -36,13 +37,20 @@ const (
|
||||
MAX_ERRORS = 10
|
||||
)
|
||||
|
||||
type TTSServResponse struct {
|
||||
ReqID string `json:"reqid"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"Message"`
|
||||
Operation string `json:"operation"`
|
||||
Sequence int `json:"sequence"`
|
||||
Data string `json:"data"`
|
||||
type V3TTSResponse struct {
|
||||
ReqID string `json:"reqid"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Event string `json:"event"`
|
||||
Sequence int `json:"sequence"`
|
||||
Data string `json:"data"`
|
||||
Sentence string `json:"sentence,omitempty"`
|
||||
IsFinal bool `json:"is_final"`
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
TextWords int `json:"text_words"`
|
||||
}
|
||||
|
||||
type OpenAITTSRequest struct {
|
||||
@@ -54,12 +62,14 @@ type OpenAITTSRequest struct {
|
||||
}
|
||||
|
||||
type ByteDanceTTSConfig struct {
|
||||
AppID string
|
||||
BearerToken string
|
||||
Cluster string
|
||||
URL string
|
||||
VoiceType string
|
||||
Timeout time.Duration
|
||||
AuthMode string
|
||||
AppID string
|
||||
AccessKey string
|
||||
ApiKey string
|
||||
ResourceId string
|
||||
Speaker string
|
||||
URL string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type RateLimiter struct {
|
||||
@@ -138,33 +148,46 @@ func (rl *RateLimiter) Allow(key string) bool {
|
||||
}
|
||||
|
||||
func initTTSConfig() error {
|
||||
authMode := os.Getenv("BYTEDANCE_TTS_AUTH_MODE")
|
||||
appID := os.Getenv("BYTEDANCE_TTS_APP_ID")
|
||||
bearerToken := os.Getenv("BYTEDANCE_TTS_BEARER_TOKEN")
|
||||
cluster := os.Getenv("BYTEDANCE_TTS_CLUSTER")
|
||||
voiceType := os.Getenv("BYTEDANCE_TTS_VOICE_TYPE")
|
||||
accessKey := os.Getenv("BYTEDANCE_TTS_ACCESS_KEY")
|
||||
apiKey := os.Getenv("BYTEDANCE_TTS_API_KEY")
|
||||
resourceId := os.Getenv("BYTEDANCE_TTS_RESOURCE_ID")
|
||||
speaker := os.Getenv("BYTEDANCE_TTS_SPEAKER")
|
||||
|
||||
missingVars := []string{}
|
||||
if appID == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_APP_ID")
|
||||
|
||||
if authMode == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_AUTH_MODE")
|
||||
} else if authMode != "legacy" && authMode != "new" {
|
||||
return fmt.Errorf("BYTEDANCE_TTS_AUTH_MODE 无效,必须是 'legacy' (旧版 AppID+AccessKey) 或 'new' (新版 ApiKey)")
|
||||
}
|
||||
if bearerToken == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_BEARER_TOKEN")
|
||||
|
||||
if resourceId == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_RESOURCE_ID")
|
||||
}
|
||||
if cluster == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_CLUSTER")
|
||||
if speaker == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_SPEAKER")
|
||||
}
|
||||
if voiceType == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_VOICE_TYPE")
|
||||
|
||||
if authMode == "legacy" {
|
||||
if appID == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_APP_ID")
|
||||
}
|
||||
if accessKey == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_ACCESS_KEY")
|
||||
}
|
||||
} else if authMode == "new" {
|
||||
if apiKey == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_API_KEY")
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingVars) > 0 {
|
||||
return fmt.Errorf("缺少必需的环境变量: %v", missingVars)
|
||||
}
|
||||
|
||||
url := os.Getenv("BYTEDANCE_TTS_ENDPOINT")
|
||||
if url == "" {
|
||||
url = "https://openspeech.bytedance.com/api/v1/tts"
|
||||
}
|
||||
url := "https://openspeech.bytedance.com/api/v3/tts/unidirectional"
|
||||
|
||||
timeout := DEFAULT_TIMEOUT
|
||||
if timeoutStr := os.Getenv("BYTEDANCE_TTS_TIMEOUT"); timeoutStr != "" {
|
||||
@@ -176,12 +199,14 @@ func initTTSConfig() error {
|
||||
}
|
||||
|
||||
ttsConfig = ByteDanceTTSConfig{
|
||||
AppID: appID,
|
||||
BearerToken: bearerToken,
|
||||
Cluster: cluster,
|
||||
URL: url,
|
||||
VoiceType: voiceType,
|
||||
Timeout: timeout,
|
||||
AuthMode: authMode,
|
||||
AppID: appID,
|
||||
AccessKey: accessKey,
|
||||
ApiKey: apiKey,
|
||||
ResourceId: resourceId,
|
||||
Speaker: speaker,
|
||||
URL: url,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -201,11 +226,19 @@ func initAPIKeys() {
|
||||
}
|
||||
|
||||
func checkEnvironmentVariables() map[string]interface{} {
|
||||
authMode := os.Getenv("BYTEDANCE_TTS_AUTH_MODE")
|
||||
|
||||
requiredVars := map[string]bool{
|
||||
"BYTEDANCE_TTS_APP_ID": os.Getenv("BYTEDANCE_TTS_APP_ID") != "",
|
||||
"BYTEDANCE_TTS_BEARER_TOKEN": os.Getenv("BYTEDANCE_TTS_BEARER_TOKEN") != "",
|
||||
"BYTEDANCE_TTS_CLUSTER": os.Getenv("BYTEDANCE_TTS_CLUSTER") != "",
|
||||
"BYTEDANCE_TTS_VOICE_TYPE": os.Getenv("BYTEDANCE_TTS_VOICE_TYPE") != "",
|
||||
"BYTEDANCE_TTS_AUTH_MODE": authMode != "",
|
||||
"BYTEDANCE_TTS_RESOURCE_ID": os.Getenv("BYTEDANCE_TTS_RESOURCE_ID") != "",
|
||||
"BYTEDANCE_TTS_SPEAKER": os.Getenv("BYTEDANCE_TTS_SPEAKER") != "",
|
||||
}
|
||||
|
||||
if authMode == "legacy" {
|
||||
requiredVars["BYTEDANCE_TTS_APP_ID"] = os.Getenv("BYTEDANCE_TTS_APP_ID") != ""
|
||||
requiredVars["BYTEDANCE_TTS_ACCESS_KEY"] = os.Getenv("BYTEDANCE_TTS_ACCESS_KEY") != ""
|
||||
} else if authMode == "new" {
|
||||
requiredVars["BYTEDANCE_TTS_API_KEY"] = os.Getenv("BYTEDANCE_TTS_API_KEY") != ""
|
||||
}
|
||||
|
||||
missingVars := []string{}
|
||||
@@ -216,10 +249,9 @@ func checkEnvironmentVariables() map[string]interface{} {
|
||||
}
|
||||
|
||||
optionalVars := map[string]bool{
|
||||
"BYTEDANCE_TTS_ENDPOINT": os.Getenv("BYTEDANCE_TTS_ENDPOINT") != "",
|
||||
"BYTEDANCE_TTS_TIMEOUT": os.Getenv("BYTEDANCE_TTS_TIMEOUT") != "",
|
||||
"OPENAI_TTS_API_KEY": os.Getenv("OPENAI_TTS_API_KEY") != "",
|
||||
"PORT": os.Getenv("PORT") != "",
|
||||
"BYTEDANCE_TTS_TIMEOUT": os.Getenv("BYTEDANCE_TTS_TIMEOUT") != "",
|
||||
"OPENAI_TTS_API_KEY": os.Getenv("OPENAI_TTS_API_KEY") != "",
|
||||
"PORT": os.Getenv("PORT") != "",
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
@@ -230,7 +262,7 @@ func checkEnvironmentVariables() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func httpPost(url string, headers map[string]string, body []byte, timeout time.Duration) ([]byte, error) {
|
||||
func httpPostStream(url string, headers map[string]string, body []byte, timeout time.Duration) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -240,50 +272,65 @@ func httpPost(url string, headers map[string]string, body []byte, timeout time.D
|
||||
}
|
||||
|
||||
client := globalHTTPClient
|
||||
if timeout != 0 && timeout != ttsConfig.Timeout {
|
||||
client = &http.Client{Timeout: timeout}
|
||||
if timeout != 0 {
|
||||
client = &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
retBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func convertSpeedToSpeechRate(speed float64) int {
|
||||
if speed <= 0.5 {
|
||||
return -50
|
||||
}
|
||||
return retBody, nil
|
||||
if speed >= 2.0 {
|
||||
return 100
|
||||
}
|
||||
return int((speed - 1.0) * 100)
|
||||
}
|
||||
|
||||
func synthesis(text string, speed float64) ([]byte, error) {
|
||||
reqID := uuid.NewString()
|
||||
params := make(map[string]map[string]interface{})
|
||||
params["app"] = make(map[string]interface{})
|
||||
params["app"]["appid"] = ttsConfig.AppID
|
||||
params["app"]["token"] = "access_token"
|
||||
params["app"]["cluster"] = ttsConfig.Cluster
|
||||
|
||||
params["user"] = make(map[string]interface{})
|
||||
params["user"]["uid"] = "uid"
|
||||
speechRate := convertSpeedToSpeechRate(speed)
|
||||
|
||||
params["audio"] = make(map[string]interface{})
|
||||
params["audio"]["voice_type"] = ttsConfig.VoiceType
|
||||
params["audio"]["encoding"] = "wav"
|
||||
params["audio"]["speed_ratio"] = speed
|
||||
params["audio"]["volume_ratio"] = 1.0
|
||||
params["audio"]["pitch_ratio"] = 1.0
|
||||
params := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"uid": "uid",
|
||||
},
|
||||
"namespace": "BidirectionalTTS",
|
||||
"req_params": map[string]interface{}{
|
||||
"text": text,
|
||||
"speaker": ttsConfig.Speaker,
|
||||
"audio_params": map[string]interface{}{
|
||||
"format": "wav",
|
||||
"sample_rate": 24000,
|
||||
"speech_rate": speechRate,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
params["request"] = make(map[string]interface{})
|
||||
params["request"]["reqid"] = reqID
|
||||
params["request"]["text"] = text
|
||||
params["request"]["text_type"] = "plain"
|
||||
params["request"]["operation"] = "query"
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Connection": "keep-alive",
|
||||
"X-Api-Resource-Id": ttsConfig.ResourceId,
|
||||
"X-Api-Request-Id": reqID,
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer;%s", ttsConfig.BearerToken)
|
||||
if ttsConfig.AuthMode == "legacy" {
|
||||
headers["X-Api-App-Id"] = ttsConfig.AppID
|
||||
headers["X-Api-Access-Key"] = ttsConfig.AccessKey
|
||||
} else {
|
||||
headers["X-Api-Key"] = ttsConfig.ApiKey
|
||||
}
|
||||
|
||||
bodyStr, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
@@ -291,30 +338,69 @@ func synthesis(text string, speed float64) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
synResp, err := httpPost(ttsConfig.URL, headers, bodyStr, ttsConfig.Timeout)
|
||||
resp, err := httpPostStream(ttsConfig.URL, headers, bodyStr, ttsConfig.Timeout)
|
||||
if err != nil {
|
||||
log.Printf("http post fail: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var respJSON TTSServResponse
|
||||
err = json.Unmarshal(synResp, &respJSON)
|
||||
if err != nil {
|
||||
log.Printf("unmarshal response fail: %v", err)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("TTS service error: status=%d, body=%s", resp.StatusCode, string(body))
|
||||
return nil, fmt.Errorf("TTS service error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var audioData []byte
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var v3Resp V3TTSResponse
|
||||
if err := json.Unmarshal(line, &v3Resp); err != nil {
|
||||
log.Printf("unmarshal chunk fail: %v, line: %s", err, string(line))
|
||||
continue
|
||||
}
|
||||
|
||||
if v3Resp.Code == 20000000 {
|
||||
if v3Resp.Usage != nil {
|
||||
log.Printf("TTS synthesis completed, usage: %+v", v3Resp.Usage)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if v3Resp.Code != 0 {
|
||||
log.Printf("TTS service error: code=%d, message=%s", v3Resp.Code, v3Resp.Message)
|
||||
return nil, fmt.Errorf("TTS service error: %s", v3Resp.Message)
|
||||
}
|
||||
|
||||
if v3Resp.Data != "" {
|
||||
chunk, err := base64.StdEncoding.DecodeString(v3Resp.Data)
|
||||
if err != nil {
|
||||
log.Printf("base64 decode fail: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
audioData = append(audioData, chunk...)
|
||||
} else if v3Resp.Sentence != "" {
|
||||
log.Printf("Received sentence info (sequence %d): %s", v3Resp.Sequence, v3Resp.Sentence)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("read stream fail: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if respJSON.Code != 3000 {
|
||||
log.Printf("TTS service error: code=%d, message=%s", respJSON.Code, respJSON.Message)
|
||||
return nil, fmt.Errorf("TTS service error")
|
||||
if len(audioData) == 0 {
|
||||
return nil, fmt.Errorf("no audio data received")
|
||||
}
|
||||
|
||||
audio, err := base64.StdEncoding.DecodeString(respJSON.Data)
|
||||
if err != nil {
|
||||
log.Printf("base64 decode fail: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return audio, nil
|
||||
return audioData, nil
|
||||
}
|
||||
|
||||
func validateAPIKey(r *http.Request) bool {
|
||||
@@ -522,7 +608,7 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"status": status,
|
||||
"service": "ByteDance TTS to OpenAI API Adapter",
|
||||
"version": "1.1.0",
|
||||
"version": "2.0.0 (v3 API)",
|
||||
"uptime": fmt.Sprintf("%.0f seconds", time.Since(startTime).Seconds()),
|
||||
"start_time": startTime.Format(time.RFC3339),
|
||||
"memory": getMemoryInfo(),
|
||||
@@ -629,9 +715,14 @@ func main() {
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
log.Printf("Starting ByteDance TTS to OpenAI API adapter server on port %s", port)
|
||||
log.Printf("Starting ByteDance TTS to OpenAI API Adapter Server")
|
||||
log.Printf("Listening on port: %s", port)
|
||||
log.Printf("OpenAI TTS endpoint: http://localhost:%s/v1/audio/speech", port)
|
||||
log.Printf("Health check: http://localhost:%s/health", port)
|
||||
log.Printf("Using ByteDance v3 API: %s", ttsConfig.URL)
|
||||
log.Printf("Auth Mode: %s", ttsConfig.AuthMode)
|
||||
log.Printf("Resource ID: %s", ttsConfig.ResourceId)
|
||||
log.Printf("Speaker: %s", ttsConfig.Speaker)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Server failed to start: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user