从Volcano-Engine-TTS迁移的原文件,后续将为其适配UI
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# 非商业用途许可协议
|
||||
|
||||
## 版权所有
|
||||
|
||||
本软件及其相关文档的版权归原作者所有。
|
||||
|
||||
## 许可条款
|
||||
|
||||
1. **非商业许可**:您被授予免费、非独占、不可转让、不可再许可的权利,仅用于非商业目的使用、复制、修改、分发、展示和运行本软件及其副本。
|
||||
|
||||
2. **禁止商用**:严禁将本软件用于任何商业目的,包括但不限于销售、租赁、提供收费服务或作为商业产品的一部分。
|
||||
|
||||
3. **保留权利**:原作者保留所有未明确授予的权利。
|
||||
|
||||
4. **免责声明**:本软件按"原样"提供,不附带任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途适用性和非侵权性的保证。在任何情况下,作者或版权持有人均不对因使用或无法使用本软件而导致的任何损害(包括但不限于直接或间接损害、或因数据丢失而导致的损害)承担责任。
|
||||
|
||||
5. **归属要求**:如果您修改并分发本软件,您必须在所有副本或实质性部分中保留原始版权声明和本许可声明。
|
||||
|
||||
6. **法律适用**:本许可的解释、效力和执行应受相关法律管辖。
|
||||
|
||||
## 定义
|
||||
|
||||
- **商业目的**:任何涉及金钱交易、产品销售、服务收费或其他经济利益的活动。
|
||||
- **非商业目的**:不涉及直接或间接经济利益的个人学习、研究、教育或非营利活动。
|
||||
|
||||
如果您对本许可有任何疑问,请联系软件作者。
|
||||
@@ -0,0 +1,139 @@
|
||||
# ByteDance TTS to OpenAI API Adapter - Linux部署指南
|
||||
|
||||
## 快速开始
|
||||
|
||||
这是为Linux环境优化的TTS服务器版本,可以将字节跳动TTS服务适配为OpenAI TTS API格式。
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `tts_server_linux.go` - Linux优化版主程序
|
||||
- `start_linux.sh` - Linux启动脚本
|
||||
- `fix_crlf.sh` - 换行符修复工具
|
||||
- `go.mod` 和 `go.sum` - Go模块依赖文件
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 上传文件到Linux服务器
|
||||
|
||||
将本文件夹中的所有文件上传到Linux服务器,建议放在`/www/wwwroot/tts-server/`目录。
|
||||
|
||||
### 2. 修复换行符(重要!)
|
||||
|
||||
在Linux服务器上,执行以下命令修复可能存在的Windows CRLF换行符问题:
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/tts-server
|
||||
chmod +x fix_crlf.sh
|
||||
./fix_crlf.sh
|
||||
```
|
||||
|
||||
### 3. 安装Go环境(如果未安装)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 4. 启动服务
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/tts-server
|
||||
chmod +x start_linux.sh
|
||||
./start_linux.sh start
|
||||
```
|
||||
|
||||
## 服务管理命令
|
||||
|
||||
- 启动服务:`./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
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
服务现在支持通过环境变量配置所有参数,推荐使用这种方式而不是直接修改代码。
|
||||
|
||||
### 必须的环境变量
|
||||
|
||||
- `BYTEDANCE_TTS_APP_ID` - 字节跳动TTS应用ID
|
||||
- `BYTEDANCE_TTS_BEARER_TOKEN` - 字节跳动TTS访问令牌
|
||||
- `BYTEDANCE_TTS_CLUSTER` - 字节跳动TTS业务集群
|
||||
- `BYTEDANCE_TTS_VOICE_TYPE` - 字节跳动TTS声音类型
|
||||
|
||||
### 可选的环境变量
|
||||
|
||||
- `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)
|
||||
|
||||
### 环境变量使用示例
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保Go版本至少为1.19
|
||||
2. 确保服务器有足够的网络权限访问字节跳动TTS服务
|
||||
3. 必须设置所有必需的环境变量,否则服务会使用默认值并显示警告信息
|
||||
4. 可以通过设置环境变量而不是直接修改代码来配置服务
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用非商业用途许可协议。您可以免费使用本软件用于非商业目的,但禁止用于任何商业活动。详细条款请参阅项目中的 [LICENSE](LICENSE) 文件。
|
||||
@@ -0,0 +1,9 @@
|
||||
module bytedance-tts-openai-adapter
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
// 移除未使用的依赖 github.com/gorilla/websocket v1.5.0
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
#!/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_linux.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 $?
|
||||
@@ -0,0 +1,608 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TTSServResponse response from backend srvs
|
||||
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"`
|
||||
}
|
||||
|
||||
// OpenAI TTS API请求格式
|
||||
type OpenAITTSRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input string `json:"input"`
|
||||
Voice string `json:"voice"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
}
|
||||
|
||||
// 字节跳动TTS配置
|
||||
type ByteDanceTTSConfig struct {
|
||||
AppID string
|
||||
BearerToken string
|
||||
Cluster string
|
||||
URL string
|
||||
VoiceType string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// API密钥配置
|
||||
var VALID_API_KEY string
|
||||
|
||||
// 全局配置
|
||||
var ttsConfig ByteDanceTTSConfig
|
||||
|
||||
// 初始化字节跳动TTS配置
|
||||
func initTTSConfig() {
|
||||
missingVars := []string{}
|
||||
|
||||
// 读取必须的环境变量
|
||||
appID := os.Getenv("BYTEDANCE_TTS_APP_ID")
|
||||
if appID == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_APP_ID")
|
||||
}
|
||||
|
||||
bearerToken := os.Getenv("BYTEDANCE_TTS_BEARER_TOKEN")
|
||||
if bearerToken == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_BEARER_TOKEN")
|
||||
}
|
||||
|
||||
cluster := os.Getenv("BYTEDANCE_TTS_CLUSTER")
|
||||
if cluster == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_CLUSTER")
|
||||
}
|
||||
|
||||
voiceType := os.Getenv("BYTEDANCE_TTS_VOICE_TYPE")
|
||||
if voiceType == "" {
|
||||
missingVars = append(missingVars, "BYTEDANCE_TTS_VOICE_TYPE")
|
||||
}
|
||||
|
||||
// 如果有缺失的必须变量,输出错误信息并使用默认值继续运行(但可能会导致功能失败)
|
||||
if len(missingVars) > 0 {
|
||||
log.Printf("警告: 缺少以下必须的环境变量: %v", missingVars)
|
||||
log.Printf("请设置这些环境变量以确保服务正常工作")
|
||||
|
||||
// 使用默认值以便服务能够启动
|
||||
if appID == "" {
|
||||
appID = "8877631864"
|
||||
}
|
||||
if bearerToken == "" {
|
||||
bearerToken = "IZFPVWC5rVIoR5vRYyc21BdJI0qNanse"
|
||||
}
|
||||
if cluster == "" {
|
||||
cluster = "volcano_icl"
|
||||
}
|
||||
if voiceType == "" {
|
||||
voiceType = "S_JuVo3sao1"
|
||||
}
|
||||
}
|
||||
|
||||
// 读取可选的环境变量,使用默认值如果未设置
|
||||
url := os.Getenv("BYTEDANCE_TTS_ENDPOINT")
|
||||
if url == "" {
|
||||
url = "https://openspeech.bytedance.com/api/v1/tts"
|
||||
} else {
|
||||
log.Printf("使用自定义字节跳动TTS端点: %s", url)
|
||||
}
|
||||
timeoutStr := os.Getenv("BYTEDANCE_TTS_TIMEOUT")
|
||||
timeout := 30 * time.Second
|
||||
if timeoutStr != "" {
|
||||
if parsedTimeout, err := time.ParseDuration(timeoutStr); err == nil {
|
||||
timeout = parsedTimeout
|
||||
log.Printf("使用自定义超时设置: %v", timeout)
|
||||
} else {
|
||||
log.Printf("无效的超时设置 '%s',使用默认值30s", timeoutStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置配置
|
||||
ttsConfig = ByteDanceTTSConfig{
|
||||
AppID: appID,
|
||||
BearerToken: bearerToken,
|
||||
Cluster: cluster,
|
||||
URL: url,
|
||||
VoiceType: voiceType,
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查环境变量配置状态
|
||||
func checkEnvironmentVariables() map[string]interface{} {
|
||||
// 检查必要的环境变量是否已设置
|
||||
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") != "",
|
||||
}
|
||||
|
||||
missingVars := []string{}
|
||||
for varName, isSet := range requiredVars {
|
||||
if !isSet {
|
||||
missingVars = append(missingVars, varName)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查可选的环境变量是否已设置
|
||||
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") != "",
|
||||
}
|
||||
|
||||
// 构建配置状态响应
|
||||
return map[string]interface{}{
|
||||
"all_required_vars_set": len(missingVars) == 0,
|
||||
"missing_required_vars": missingVars,
|
||||
"required_vars": requiredVars, // 只显示是否设置,不显示具体值
|
||||
"optional_vars": optionalVars, // 只显示是否设置,不显示具体值
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func httpPost(url string, headers map[string]string, body []byte, timeout time.Duration) ([]byte, error) {
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
retBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return retBody, err
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
params["audio"] = make(map[string]interface{})
|
||||
params["audio"]["voice_type"] = ttsConfig.VoiceType
|
||||
params["audio"]["encoding"] = "wav"
|
||||
|
||||
// 处理语速参数
|
||||
if speed <= 0 {
|
||||
speed = 1.0
|
||||
}
|
||||
params["audio"]["speed_ratio"] = speed
|
||||
params["audio"]["volume_ratio"] = 1.0
|
||||
params["audio"]["pitch_ratio"] = 1.0
|
||||
|
||||
params["request"] = make(map[string]interface{})
|
||||
params["request"]["reqid"] = reqID
|
||||
params["request"]["text"] = text
|
||||
params["request"]["text_type"] = "plain"
|
||||
params["request"]["operation"] = "query"
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer;%s", ttsConfig.BearerToken)
|
||||
|
||||
bodyStr, _ := json.Marshal(params)
|
||||
synResp, err := httpPost(ttsConfig.URL, headers, []byte(bodyStr), ttsConfig.Timeout)
|
||||
if err != nil {
|
||||
log.Printf("http post fail [err:%s]\n", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respJSON TTSServResponse
|
||||
err = json.Unmarshal(synResp, &respJSON)
|
||||
if err != nil {
|
||||
log.Printf("unmarshal response fail [err:%s]\n", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if respJSON.Code != 3000 {
|
||||
log.Printf("code fail [code:%d, message:%s]\n", respJSON.Code, respJSON.Message)
|
||||
return nil, fmt.Errorf("TTS service error: code %d, message: %s", respJSON.Code, respJSON.Message)
|
||||
}
|
||||
|
||||
audio, err := base64.StdEncoding.DecodeString(respJSON.Data)
|
||||
if err != nil {
|
||||
log.Printf("base64 decode fail [err:%s]\n", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return audio, nil
|
||||
}
|
||||
|
||||
// 验证API密钥
|
||||
func validateAPIKey(r *http.Request) bool {
|
||||
// 当VALID_API_KEY为空时,允许任何API密钥通过验证
|
||||
if VALID_API_KEY == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 从 Authorization header 中获取 Bearer token
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否以 "Bearer " 开头
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 提取token
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
return token == VALID_API_KEY
|
||||
}
|
||||
|
||||
// OpenAI TTS API兼容端点
|
||||
func openaiTTSHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证API密钥
|
||||
if !validateAPIKey(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"message": "Invalid API key provided.",
|
||||
"type": "invalid_request_error",
|
||||
"code": "invalid_api_key",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req OpenAITTSRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Input == "" {
|
||||
http.Error(w, "Input text is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认语速
|
||||
speed := req.Speed
|
||||
if speed <= 0 {
|
||||
speed = 1.0
|
||||
}
|
||||
|
||||
// 调用字节跳动TTS
|
||||
ttsStart := time.Now()
|
||||
audioData, err := synthesis(req.Input, speed)
|
||||
// 记录TTS处理时间
|
||||
_ = time.Since(ttsStart)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("TTS synthesis failed: %v", err)
|
||||
http.Error(w, "TTS synthesis failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
w.Header().Set("Content-Type", "audio/wav")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(audioData)))
|
||||
|
||||
// 返回音频数据
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(audioData)
|
||||
}
|
||||
|
||||
// 统计数据结构体
|
||||
type Stats struct {
|
||||
totalRequests int64
|
||||
successfulRequests int64
|
||||
failedRequests int64
|
||||
totalResponseTime time.Duration
|
||||
recentResponseTimes []float64
|
||||
maxRecentResponses int
|
||||
lastErrors []string
|
||||
maxLastErrors int
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// API调用统计
|
||||
var apiStats = &Stats{
|
||||
recentResponseTimes: make([]float64, 0, 100),
|
||||
maxRecentResponses: 100,
|
||||
lastErrors: make([]string, 0, 10),
|
||||
maxLastErrors: 10,
|
||||
}
|
||||
|
||||
// 添加请求统计
|
||||
func addRequestStats(success bool, responseTime time.Duration, errMsg string) {
|
||||
apiStats.mutex.Lock()
|
||||
defer apiStats.mutex.Unlock()
|
||||
|
||||
apiStats.totalRequests++
|
||||
apiStats.totalResponseTime += responseTime
|
||||
|
||||
// 添加到最近响应时间数组
|
||||
apiStats.recentResponseTimes = append(apiStats.recentResponseTimes, responseTime.Seconds()*1000) // 转换为毫秒
|
||||
if len(apiStats.recentResponseTimes) > apiStats.maxRecentResponses {
|
||||
apiStats.recentResponseTimes = apiStats.recentResponseTimes[1:]
|
||||
}
|
||||
|
||||
if success {
|
||||
apiStats.successfulRequests++
|
||||
} else {
|
||||
apiStats.failedRequests++
|
||||
// 添加到最近错误数组
|
||||
if errMsg != "" {
|
||||
errInfo := fmt.Sprintf("%s: %s", time.Now().Format(time.RFC3339), errMsg)
|
||||
apiStats.lastErrors = append(apiStats.lastErrors, errInfo)
|
||||
if len(apiStats.lastErrors) > apiStats.maxLastErrors {
|
||||
apiStats.lastErrors = apiStats.lastErrors[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取内存信息
|
||||
func getMemoryInfo() map[string]uint64 {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return map[string]uint64{
|
||||
"total": m.Sys,
|
||||
"allocated": m.Alloc,
|
||||
"available": m.Sys - m.Alloc,
|
||||
"goroutines": uint64(runtime.NumGoroutine()),
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网络信息
|
||||
func getNetworkInfo() map[string]interface{} {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
interfaces := make([]map[string]interface{}, 0, len(ifaces))
|
||||
for _, iface := range ifaces {
|
||||
addrs, _ := iface.Addrs()
|
||||
addresses := make([]string, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
addresses = append(addresses, addr.String())
|
||||
}
|
||||
interfaces = append(interfaces, map[string]interface{}{
|
||||
"name": iface.Name,
|
||||
"mac": iface.HardwareAddr.String(),
|
||||
"addresses": addresses,
|
||||
"up": (iface.Flags & net.FlagUp) != 0,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"interfaces": interfaces,
|
||||
}
|
||||
}
|
||||
|
||||
// 健康检查端点
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// 收集统计数据
|
||||
apiStats.mutex.RLock()
|
||||
totalRequests := apiStats.totalRequests
|
||||
successfulRequests := apiStats.successfulRequests
|
||||
failedRequests := apiStats.failedRequests
|
||||
totalResponseTime := apiStats.totalResponseTime
|
||||
recentResponseTimes := make([]float64, len(apiStats.recentResponseTimes))
|
||||
copy(recentResponseTimes, apiStats.recentResponseTimes)
|
||||
lastErrors := make([]string, len(apiStats.lastErrors))
|
||||
copy(lastErrors, apiStats.lastErrors)
|
||||
apiStats.mutex.RUnlock()
|
||||
|
||||
// 计算错误率
|
||||
var errorRate float64
|
||||
if totalRequests > 0 {
|
||||
errorRate = float64(failedRequests) / float64(totalRequests) * 100
|
||||
}
|
||||
|
||||
// 计算平均响应时间
|
||||
var avgResponseTime float64
|
||||
if totalRequests > 0 {
|
||||
avgResponseTime = totalResponseTime.Seconds() * 1000 / float64(totalRequests) // 毫秒
|
||||
}
|
||||
|
||||
// 检查环境变量配置
|
||||
envCheckStatus := checkEnvironmentVariables()
|
||||
allEnvVarsSet := envCheckStatus["all_required_vars_set"].(bool)
|
||||
|
||||
// 确定服务状态
|
||||
status := "ok"
|
||||
if !allEnvVarsSet {
|
||||
status = "configuration_error"
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := map[string]interface{}{
|
||||
"status": status,
|
||||
"service": "ByteDance TTS to OpenAI API Adapter",
|
||||
"version": "1.0.0",
|
||||
"uptime": fmt.Sprintf("%.0f seconds", time.Since(startTime).Seconds()),
|
||||
"start_time": startTime.Format(time.RFC3339),
|
||||
"pid": os.Getpid(),
|
||||
"memory": getMemoryInfo(),
|
||||
"network": getNetworkInfo(),
|
||||
"api_stats": map[string]interface{}{
|
||||
"total_requests": totalRequests,
|
||||
"successful_requests": successfulRequests,
|
||||
"failed_requests": failedRequests,
|
||||
"error_rate": fmt.Sprintf("%.2f%%", errorRate),
|
||||
"avg_response_time_ms": fmt.Sprintf("%.2f", avgResponseTime),
|
||||
"recent_response_times_ms": recentResponseTimes,
|
||||
"concurrent_requests": runtime.NumGoroutine() - 1, // 减去健康检查本身的goroutine
|
||||
},
|
||||
"errors": map[string]interface{}{
|
||||
"recent_errors": lastErrors,
|
||||
},
|
||||
"config_status": envCheckStatus,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
var startTime time.Time
|
||||
|
||||
// 自定义ResponseWriter以捕获状态码
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rec *statusRecorder) WriteHeader(code int) {
|
||||
rec.statusCode = code
|
||||
rec.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func main() {
|
||||
startTime = time.Now()
|
||||
|
||||
// 从环境变量读取API密钥,当环境变量未设置时,设为空字符串(允许任意key)
|
||||
VALID_API_KEY = os.Getenv("OPENAI_TTS_API_KEY")
|
||||
if VALID_API_KEY == "" {
|
||||
log.Println("Warning: OPENAI_TTS_API_KEY environment variable not set. All API keys will be accepted.")
|
||||
}
|
||||
|
||||
// 初始化字节跳动TTS配置
|
||||
initTTSConfig()
|
||||
|
||||
// 设置日志格式
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetPrefix("[TTS-Server] ")
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
||||
// 添加中间件:请求日志和统计
|
||||
router.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 跳过健康检查的统计,避免递归
|
||||
if r.URL.Path == "/health" {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("%s %s %s %v", r.Method, r.RequestURI, r.RemoteAddr, time.Since(start))
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
rec := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
next.ServeHTTP(rec, r)
|
||||
duration := time.Since(start)
|
||||
|
||||
// 记录日志
|
||||
log.Printf("%s %s %s %d %v", r.Method, r.RequestURI, r.RemoteAddr, rec.statusCode, duration)
|
||||
|
||||
// 更新统计
|
||||
success := rec.statusCode >= 200 && rec.statusCode < 400
|
||||
errMsg := ""
|
||||
if !success {
|
||||
errMsg = fmt.Sprintf("HTTP %d", rec.statusCode)
|
||||
}
|
||||
addRequestStats(success, duration, errMsg)
|
||||
})
|
||||
})
|
||||
|
||||
// OpenAI TTS API兼容端点
|
||||
router.HandleFunc("/v1/audio/speech", openaiTTSHandler).Methods("POST")
|
||||
|
||||
// 健康检查
|
||||
router.HandleFunc("/health", healthHandler).Methods("GET")
|
||||
|
||||
// 根路径重定向到健康检查
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/health", http.StatusFound)
|
||||
}).Methods("GET")
|
||||
|
||||
port := ":8080"
|
||||
// 从环境变量获取端口配置
|
||||
if envPort := os.Getenv("PORT"); envPort != "" {
|
||||
port = ":" + envPort
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: port,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// 创建一个通道来接收系统信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// 在goroutine中启动服务器
|
||||
go func() {
|
||||
log.Printf("Starting ByteDance TTS to OpenAI API adapter server 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("PID: %d", os.Getpid())
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Server failed to start: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待信号
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
// 创建一个5秒的超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 优雅关闭服务器
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Printf("Server forced to shutdown: %v", err)
|
||||
} else {
|
||||
log.Println("Server exited gracefully")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user