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 }