feat: 升级项目适配火山引擎TTS v3 API

新增:
- 添加.env.example环境变量配置示例
- 重构鉴权逻辑,支持新旧两种鉴权模式
- 适配v3版API接口,实现流式响应
- 完善README文档和配置说明

移除:
- 删除原Linux启动脚本start_linux.sh

优化:
- 重构代码结构,适配新的API参数格式
- 优化日志输出和错误处理
This commit is contained in:
sun
2026-05-17 18:08:06 +08:00
parent 25e09cd54f
commit 3304707abe
4 changed files with 434 additions and 486 deletions
+56
View File
@@ -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
+196 -103
View File
@@ -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
View File
@@ -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 $?
+163 -72
View File
@@ -1,6 +1,7 @@
package main
import (
"bufio"
"bytes"
"context"
"encoding/base64"
@@ -36,13 +37,20 @@ const (
MAX_ERRORS = 10
)
type TTSServResponse struct {
type V3TTSResponse struct {
ReqID string `json:"reqid"`
Code int `json:"code"`
Message string `json:"Message"`
Operation string `json:"operation"`
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,11 +62,13 @@ type OpenAITTSRequest struct {
}
type ByteDanceTTSConfig struct {
AuthMode string
AppID string
BearerToken string
Cluster string
AccessKey string
ApiKey string
ResourceId string
Speaker string
URL string
VoiceType string
Timeout time.Duration
}
@@ -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 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 resourceId == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_RESOURCE_ID")
}
if speaker == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_SPEAKER")
}
if authMode == "legacy" {
if appID == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_APP_ID")
}
if bearerToken == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_BEARER_TOKEN")
if accessKey == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_ACCESS_KEY")
}
if cluster == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_CLUSTER")
} else if authMode == "new" {
if apiKey == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_API_KEY")
}
if voiceType == "" {
missingVars = append(missingVars, "BYTEDANCE_TTS_VOICE_TYPE")
}
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,11 +199,13 @@ func initTTSConfig() error {
}
ttsConfig = ByteDanceTTSConfig{
AuthMode: authMode,
AppID: appID,
BearerToken: bearerToken,
Cluster: cluster,
AccessKey: accessKey,
ApiKey: apiKey,
ResourceId: resourceId,
Speaker: speaker,
URL: url,
VoiceType: voiceType,
Timeout: timeout,
}
@@ -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,7 +249,6 @@ 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") != "",
@@ -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
return client.Do(req)
}
defer resp.Body.Close()
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)
return nil, 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)
}
if respJSON.Code != 3000 {
log.Printf("TTS service error: code=%d, message=%s", respJSON.Code, respJSON.Message)
return nil, fmt.Errorf("TTS service error")
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
}
audio, err := base64.StdEncoding.DecodeString(respJSON.Data)
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
}
return audio, nil
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 len(audioData) == 0 {
return nil, fmt.Errorf("no audio data received")
}
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)