286 lines
7.9 KiB
Go
286 lines
7.9 KiB
Go
package radio
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"sundynix-go/global"
|
|
"sundynix-go/model/radio"
|
|
"sundynix-go/model/system"
|
|
"sundynix-go/utils/upload"
|
|
|
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
|
tts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tts/v20190823"
|
|
)
|
|
|
|
// TTSService TTS服务
|
|
type TTSService struct{}
|
|
|
|
// TTSTextToSpeechRequest TTS请求参数
|
|
type TTSTextToSpeechRequest struct {
|
|
Text string // 要转换的文本
|
|
VoiceType int // 声音类型
|
|
Speed int // 语速 -2到6
|
|
Volume int // 音量 -10到10
|
|
ProgramId string // 节目ID
|
|
}
|
|
|
|
// TTSResult TTS任务结果
|
|
type TTSResult struct {
|
|
TaskId string // 任务ID
|
|
ProgramId string // 节目ID
|
|
Status int // 0:处理中 1:成功 2:失败
|
|
StatusMsg string // 状态描述
|
|
AudioId string // OSS文件ID
|
|
AudioUrl string // OSS文件URL
|
|
CreatedAt time.Time // 创建时间
|
|
}
|
|
|
|
var TTSServiceApp = new(TTSService)
|
|
|
|
// newTTSClient 创建腾讯云TTS客户端
|
|
func (t *TTSService) newTTSClient() (*tts.Client, error) {
|
|
credential := common.NewCredential(
|
|
global.Config.TTS.SecretId,
|
|
global.Config.TTS.SecretKey,
|
|
)
|
|
cpf := profile.NewClientProfile()
|
|
cpf.HttpProfile.Endpoint = "tts.tencentcloudapi.com"
|
|
return tts.NewClient(credential, "ap-guangzhou", cpf)
|
|
}
|
|
|
|
// SubmitTTSTask 提交长文本TTS任务 (异步)
|
|
func (t *TTSService) SubmitTTSTask(req TTSTextToSpeechRequest) (string, error) {
|
|
if req.Text == "" {
|
|
return "", fmt.Errorf("文本内容不能为空")
|
|
}
|
|
|
|
// 提交长文本TTS任务
|
|
taskId, err := t.doSubmitTTSTask(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("提交TTS任务失败: %v", err)
|
|
}
|
|
|
|
// 异步处理结果
|
|
go t.asyncProcessResult(req.ProgramId, taskId)
|
|
|
|
return taskId, nil
|
|
}
|
|
|
|
// asyncProcessResult 异步处理TTS结果
|
|
func (t *TTSService) asyncProcessResult(programId, taskId string) {
|
|
// 轮询查询任务结果,获取音频下载URL
|
|
resultUrl, err := t.waitForResult(taskId)
|
|
if err != nil {
|
|
global.Logger.Error(fmt.Sprintf("TTS任务失败, TaskId: %s, Error: %v", taskId, err))
|
|
return
|
|
}
|
|
|
|
// 从ResultUrl下载音频数据
|
|
audioData, err := t.downloadAudio(resultUrl)
|
|
if err != nil {
|
|
global.Logger.Error(fmt.Sprintf("下载音频失败, TaskId: %s, Error: %v", taskId, err))
|
|
return
|
|
}
|
|
|
|
// 上传到OSS
|
|
audioId, err := t.uploadToOSS(audioData, programId)
|
|
if err != nil {
|
|
global.Logger.Error(fmt.Sprintf("上传OSS失败, TaskId: %s, Error: %v", taskId, err))
|
|
return
|
|
}
|
|
|
|
// 更新节目的audio_id
|
|
if err := global.DB.Model(&radio.RadioProgram{}).Where("id = ?", programId).
|
|
Update("audio_id", audioId).Error; err != nil {
|
|
global.Logger.Error(fmt.Sprintf("更新节目音频ID失败, TaskId: %s, Error: %v", taskId, err))
|
|
return
|
|
}
|
|
|
|
global.Logger.Info(fmt.Sprintf("TTS任务完成, TaskId: %s, ProgramId: %s, AudioId: %s", taskId, programId, audioId))
|
|
}
|
|
|
|
// SubmitTTSTaskOnly 仅提交任务,返回TaskId (供外部调用)
|
|
func (t *TTSService) SubmitTTSTaskOnly(req TTSTextToSpeechRequest) (string, error) {
|
|
if req.Text == "" {
|
|
return "", fmt.Errorf("文本内容不能为空")
|
|
}
|
|
return t.doSubmitTTSTask(req)
|
|
}
|
|
|
|
// QueryTTSTask 查询任务状态
|
|
func (t *TTSService) QueryTTSTask(taskId string) (int, string, error) {
|
|
_, status, statusMsg, err := t.queryTaskResult(taskId)
|
|
return status, statusMsg, err
|
|
}
|
|
|
|
// GetAudioByTaskId 根据TaskId获取音频下载URL
|
|
func (t *TTSService) GetAudioByTaskId(taskId string) ([]byte, error) {
|
|
resultUrl, status, _, err := t.queryTaskResult(taskId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status != 1 {
|
|
return nil, fmt.Errorf("任务未完成或失败")
|
|
}
|
|
return t.downloadAudio(resultUrl)
|
|
}
|
|
|
|
// doSubmitTTSTask 使用官方SDK提交长文本TTS任务
|
|
func (t *TTSService) doSubmitTTSTask(req TTSTextToSpeechRequest) (string, error) {
|
|
client, err := t.newTTSClient()
|
|
if err != nil {
|
|
return "", fmt.Errorf("创建TTS客户端失败: %v", err)
|
|
}
|
|
|
|
request := tts.NewCreateTtsTaskRequest()
|
|
request.Text = common.StringPtr(req.Text)
|
|
request.VoiceType = common.Int64Ptr(int64(req.VoiceType))
|
|
request.Speed = common.Float64Ptr(float64(req.Speed))
|
|
request.Volume = common.Float64Ptr(float64(req.Volume))
|
|
request.Codec = common.StringPtr("mp3")
|
|
request.ModelType = common.Int64Ptr(1)
|
|
|
|
response, err := client.CreateTtsTask(request)
|
|
if err != nil {
|
|
return "", fmt.Errorf("调用CreateTtsTask失败: %v", err)
|
|
}
|
|
|
|
if response.Response == nil || response.Response.Data == nil || response.Response.Data.TaskId == nil {
|
|
return "", fmt.Errorf("未获取到TaskId")
|
|
}
|
|
|
|
return *response.Response.Data.TaskId, nil
|
|
}
|
|
|
|
// waitForResult 轮询查询任务结果,返回音频下载URL
|
|
func (t *TTSService) waitForResult(taskId string) (string, error) {
|
|
maxRetries := 30
|
|
interval := 15 * time.Second
|
|
|
|
for i := 0; i < maxRetries; i++ {
|
|
time.Sleep(interval)
|
|
|
|
resultUrl, status, statusMsg, err := t.queryTaskResult(taskId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
switch status {
|
|
case 1: // 成功
|
|
return resultUrl, nil
|
|
case 2: // 失败
|
|
return "", fmt.Errorf("TTS合成失败: %s", statusMsg)
|
|
default:
|
|
global.Logger.Debug(fmt.Sprintf("TTS任务处理中, TaskId: %s, 重试次数: %d", taskId, i+1))
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("TTS任务超时, TaskId: %s", taskId)
|
|
}
|
|
|
|
// queryTaskResult 使用官方SDK查询任务结果
|
|
// 返回: resultUrl, status, statusMsg, error
|
|
// status: 0-等待中 1-成功 2-失败 3-执行中
|
|
func (t *TTSService) queryTaskResult(taskId string) (string, int, string, error) {
|
|
client, err := t.newTTSClient()
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("创建TTS客户端失败: %v", err)
|
|
}
|
|
|
|
request := tts.NewDescribeTtsTaskStatusRequest()
|
|
request.TaskId = common.StringPtr(taskId)
|
|
|
|
response, err := client.DescribeTtsTaskStatus(request)
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("调用DescribeTtsTaskStatus失败: %v", err)
|
|
}
|
|
|
|
if response.Response == nil || response.Response.Data == nil {
|
|
return "", 0, "", fmt.Errorf("查询任务状态返回为空")
|
|
}
|
|
|
|
data := response.Response.Data
|
|
|
|
// 将SDK的StatusStr转换为数字状态
|
|
status := 0
|
|
statusMsg := ""
|
|
if data.StatusStr != nil {
|
|
statusMsg = *data.StatusStr
|
|
switch *data.StatusStr {
|
|
case "success":
|
|
status = 1
|
|
case "failed":
|
|
status = 2
|
|
case "waiting", "doing":
|
|
status = 0
|
|
}
|
|
}
|
|
|
|
resultUrl := ""
|
|
if data.ResultUrl != nil {
|
|
resultUrl = *data.ResultUrl
|
|
}
|
|
|
|
return resultUrl, status, statusMsg, nil
|
|
}
|
|
|
|
// downloadAudio 从URL下载音频数据
|
|
func (t *TTSService) downloadAudio(audioUrl string) ([]byte, error) {
|
|
resp, err := http.Get(audioUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("下载音频失败: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("下载音频HTTP错误: %d", resp.StatusCode)
|
|
}
|
|
|
|
audioData, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取音频数据失败: %v", err)
|
|
}
|
|
|
|
return audioData, nil
|
|
}
|
|
|
|
// uploadToOSS 上传音频到OSS并保存到数据库
|
|
func (t *TTSService) uploadToOSS(audioData []byte, programId string) (string, error) {
|
|
instance := upload.OssInstance()
|
|
minioClient, ok := instance.(*upload.Minio)
|
|
if !ok {
|
|
return "", fmt.Errorf("获取MinIO客户端失败")
|
|
}
|
|
timestamp := time.Now().UnixMicro()
|
|
timestr := strconv.FormatInt(timestamp, 10)
|
|
key := fmt.Sprintf("audio/%s/%s.mp3", time.Now().Format("2006-01-02"), programId+"-"+timestr)
|
|
filename := fmt.Sprintf("program-%s.mp3", programId)
|
|
|
|
fileURL, err := minioClient.UploadBytes(audioData, key, "audio/mpeg")
|
|
if err != nil {
|
|
return "", fmt.Errorf("上传文件到OSS失败: %v", err)
|
|
}
|
|
|
|
hashStr := fmt.Sprintf("%x", md5.Sum(audioData))
|
|
|
|
oss := system.Oss{
|
|
Name: filename,
|
|
Url: fileURL,
|
|
Key: key,
|
|
Suffix: "mp3",
|
|
Tag: "mp3",
|
|
MD5: hashStr,
|
|
}
|
|
if err := global.DB.Create(&oss).Error; err != nil {
|
|
return "", fmt.Errorf("保存文件记录失败: %v", err)
|
|
}
|
|
|
|
return oss.Id, nil
|
|
}
|