All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 添加 SoundWithContext 方法,使用请求 context 而非全局 context - 修复 TTS 使用服务器全局 context 导致无法取消的问题 - 添加详细的诊断日志(解码、播放、TTS 合成各阶段) - 检测并记录 TTS 合成数据为空的情况 修复前 TTS 播放使用全局 context,当播放卡住时无法通过超时 或取消机制中断,导致后续任务永远无法执行。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
3.8 KiB
Go
158 lines
3.8 KiB
Go
package tts
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"game-driver/config"
|
||
"game-driver/leaf"
|
||
"game-driver/pkg/audio"
|
||
"game-driver/pkg/errorsx"
|
||
"go.uber.org/zap"
|
||
"io"
|
||
"log"
|
||
"time"
|
||
|
||
nls "github.com/aliyun/alibabacloud-nls-go-sdk"
|
||
)
|
||
|
||
type AliTTS struct {
|
||
config.AliyunConfig
|
||
ctx context.Context
|
||
tokenResult nls.TokenResult
|
||
}
|
||
|
||
type result struct {
|
||
Data io.ReadWriter
|
||
Error error
|
||
}
|
||
|
||
var DefaultTTS = &AliTTS{}
|
||
|
||
// onTaskFailed 识别过程中的错误处理回调参数
|
||
func (tts *AliTTS) onTaskFailed(text string, param interface{}) {
|
||
p, _ := param.(*result)
|
||
p.Error = fmt.Errorf("语音合成异常: %v", text)
|
||
}
|
||
|
||
// onSynthesisResult 语音合成数据回调参数
|
||
func (tts *AliTTS) onSynthesisResult(data []byte, param interface{}) {
|
||
p, _ := param.(*result)
|
||
p.Data.Write(data)
|
||
}
|
||
|
||
func (tts *AliTTS) Sound(text string) {
|
||
if text == "" {
|
||
return
|
||
}
|
||
zap.S().Infof("开始播放TTS: %s", text)
|
||
buf, err := tts.Get(text)
|
||
if err == nil && buf != nil {
|
||
zap.S().Debugln("TTS合成成功,开始播放")
|
||
audio.PlayWav(tts.ctx, buf)
|
||
zap.S().Infof("TTS播放完成: %s", text)
|
||
} else {
|
||
zap.S().Errorln("AliTTS 请求异常: ", err)
|
||
}
|
||
}
|
||
|
||
// SoundWithContext 使用指定的 context 播放 TTS,支持取消和超时
|
||
func (tts *AliTTS) SoundWithContext(ctx context.Context, text string) {
|
||
if text == "" {
|
||
return
|
||
}
|
||
zap.S().Infof("开始播放TTS: %s", text)
|
||
buf, err := tts.Get(text)
|
||
if err == nil && buf != nil {
|
||
zap.S().Debugln("TTS合成成功,开始播放")
|
||
audio.PlayWav(ctx, buf)
|
||
zap.S().Infof("TTS播放完成: %s", text)
|
||
} else {
|
||
zap.S().Errorln("AliTTS 请求异常: ", err)
|
||
}
|
||
}
|
||
|
||
func (tts *AliTTS) getToken() error {
|
||
if tts.tokenResult.ExpireTime != 0 && time.Unix(tts.tokenResult.ExpireTime, 0).After(time.Now()) {
|
||
return nil
|
||
}
|
||
tts.tokenResult = nls.TokenResult{}
|
||
resultMessage, err := nls.GetToken("cn-shanghai", "nls-meta.cn-shanghai.aliyuncs.com", tts.AccessKeyID, tts.AccessKeySecret, "2019-02-28")
|
||
if err != nil {
|
||
return err
|
||
} else if resultMessage.ErrMsg != "" {
|
||
zap.S().Errorf("获取Token失败: %s", resultMessage.ErrMsg)
|
||
return errorsx.ThirdPartyErr
|
||
}
|
||
tts.tokenResult = resultMessage.TokenResult
|
||
return nil
|
||
}
|
||
|
||
func (tts *AliTTS) Get(text string) (io.Reader, error) {
|
||
param := nls.DefaultSpeechSynthesisParam()
|
||
param.Volume = tts.Volume
|
||
param.Voice = tts.Voice
|
||
param.SpeechRate = tts.SpeechRate
|
||
|
||
err := tts.getToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
connectConfig := nls.NewConnectionConfigWithToken(nls.DEFAULT_URL, tts.AppKey, tts.tokenResult.Id)
|
||
|
||
logger := nls.NewNlsLogger(leaf.DefaultWriter, "", log.LstdFlags|log.Ltime)
|
||
logger.SetLogSil(false)
|
||
logger.SetDebug(true)
|
||
|
||
ttsData := &result{
|
||
Data: &bytes.Buffer{},
|
||
}
|
||
|
||
synthesis, err := nls.NewSpeechSynthesis(
|
||
connectConfig, logger, false,
|
||
tts.onTaskFailed, tts.onSynthesisResult, nil,
|
||
nil, nil, ttsData,
|
||
)
|
||
if err != nil {
|
||
return ttsData.Data, err
|
||
}
|
||
defer synthesis.Shutdown()
|
||
|
||
ch, err := synthesis.Start(text, param, nil)
|
||
if err != nil {
|
||
return ttsData.Data, err
|
||
}
|
||
|
||
// 等待语音合成结束
|
||
select {
|
||
case done := <-ch:
|
||
{
|
||
if !done {
|
||
zap.S().Errorln("TTS合成失败: done=false")
|
||
return ttsData.Data, errorsx.ThirdPartyErr
|
||
}
|
||
size := ttsData.Data.(*bytes.Buffer).Len()
|
||
zap.S().Debugf("TTS合成成功,数据大小: %d bytes", size)
|
||
if size == 0 {
|
||
zap.S().Errorln("TTS合成数据为空")
|
||
return ttsData.Data, errorsx.ThirdPartyErr
|
||
}
|
||
return ttsData.Data, nil
|
||
}
|
||
case <-time.After(time.Duration(tts.Timeout) * time.Second):
|
||
zap.S().Errorln("TTS合成超时")
|
||
return ttsData.Data, errorsx.DriverTimeoutErr
|
||
case <-tts.ctx.Done():
|
||
zap.S().Errorln("TTS合成被取消")
|
||
return ttsData.Data, errorsx.DriverCancelErr
|
||
}
|
||
}
|
||
|
||
func New(ctx context.Context, config config.AliyunConfig) *AliTTS {
|
||
return &AliTTS{
|
||
ctx: ctx,
|
||
AliyunConfig: config,
|
||
}
|
||
}
|