refactor(tts): TTS 极简重构与代码质量提升
## 核心改进 ### TTS 模块重构 - 统一 API,仅保留 Sound(ctx, text) 方法 - 优化日志,添加 [TTS] 前缀和结构化字段 - 实现互斥等待:同时只播放一个,新请求等待旧播放完成 - 响应 context 取消:超时或断开时立即停止播放 - 移除全局 context 存储,改为参数传递 - 简化实例化:New(config) 无需传入 context ### 代码质量提升 - 修复 PlayWav/PlayMP3 的死循环 bug(context 取消时缺少 return) - 修复 standby_ctrl/pause.go 的忙循环(添加 Sleep 避免CPU 100%) - 添加关键路径错误传播(only_video.go 不再忽略播放错误) - 新增 pkg/errorsx/handler.go 统一错误处理工具 ## 代码优化 - TTS 代码从 234 行精简到 166 行(减少 29%) - 移除冗余状态管理(playing 标志、等待循环) - 利用互斥锁的阻塞特性实现优雅等待 - 保持简洁易读的代码风格 ## 行为说明 ✅ 同时只能播放一个 TTS(互斥) ✅ 新请求等待当前播放完成(不打断) ✅ 响应 context 取消(超时停止) ✅ 日志完善,便于排查问题 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
57
pkg/errorsx/handler.go
Normal file
57
pkg/errorsx/handler.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package errorsx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// ErrorHandler 错误处理接口
|
||||
type ErrorHandler interface {
|
||||
HandleError(error, string)
|
||||
}
|
||||
|
||||
// DefaultHandler 默认错误处理器
|
||||
type DefaultHandler struct{}
|
||||
|
||||
// HandleError 处理错误并记录日志
|
||||
func (h *DefaultHandler) HandleError(err error, context string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录错误信息和调用栈
|
||||
zap.S().Errorw(
|
||||
fmt.Sprintf("%s: %v", context, err),
|
||||
"stack", string(debug.Stack()),
|
||||
)
|
||||
}
|
||||
|
||||
// Wrap 错误包装,保留调用链
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %w", message, err)
|
||||
}
|
||||
|
||||
// Must panic 如果 err 不为 nil
|
||||
func Must(err error, message string) {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: %v", message, err))
|
||||
}
|
||||
}
|
||||
|
||||
// LogError 记录错误但不中断
|
||||
func LogError(err error, context string) {
|
||||
if err != nil {
|
||||
zap.S().Errorw(fmt.Sprintf("%s: %v", context, err))
|
||||
}
|
||||
}
|
||||
|
||||
// LogWarn 记录警告
|
||||
func LogWarn(err error, context string) {
|
||||
if err != nil {
|
||||
zap.S().Warnw(fmt.Sprintf("%s: %v", context, err))
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,18 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
nls "github.com/aliyun/alibabacloud-nls-go-sdk"
|
||||
)
|
||||
|
||||
// AliTTS 阿里云语音合成
|
||||
// 同一时间只能播放一个 TTS
|
||||
type AliTTS struct {
|
||||
config.AliyunConfig
|
||||
ctx context.Context
|
||||
tokenResult nls.TokenResult
|
||||
mu sync.Mutex // 互斥锁,确保同时只播放一个
|
||||
}
|
||||
|
||||
type result struct {
|
||||
@@ -29,78 +32,86 @@ type result struct {
|
||||
|
||||
var DefaultTTS = &AliTTS{}
|
||||
|
||||
// onTaskFailed 识别过程中的错误处理回调参数
|
||||
// onTaskFailed TTS 合成失败回调
|
||||
func (tts *AliTTS) onTaskFailed(text string, param interface{}) {
|
||||
p, _ := param.(*result)
|
||||
p.Error = fmt.Errorf("语音合成异常: %v", text)
|
||||
}
|
||||
|
||||
// onSynthesisResult 语音合成数据回调参数
|
||||
// onSynthesisResult TTS 合成数据回调
|
||||
func (tts *AliTTS) onSynthesisResult(data []byte, param interface{}) {
|
||||
p, _ := param.(*result)
|
||||
p.Data.Write(data)
|
||||
}
|
||||
|
||||
func (tts *AliTTS) Sound(text string) {
|
||||
// Sound 播放 TTS
|
||||
// 如果已有 TTS 在播放,会等待当前播放完成后再播放新的
|
||||
func (tts *AliTTS) Sound(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(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 == "" {
|
||||
zap.S().Infof("[TTS] 开始播放: %s", text)
|
||||
|
||||
buf, err := tts.Get(ctx, text)
|
||||
if err != nil {
|
||||
zap.S().Errorw("[TTS] 合成失败", "text", text, "error", err)
|
||||
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)
|
||||
|
||||
size := buf.(*bytes.Buffer).Len()
|
||||
zap.S().Debugf("[TTS] 合成成功: %s (%d字节)", text, size)
|
||||
|
||||
// 获取锁,阻塞等待直到可以播放
|
||||
zap.S().Debugf("[TTS] 等待播放锁: %s", text)
|
||||
tts.mu.Lock()
|
||||
defer tts.mu.Unlock()
|
||||
|
||||
audio.PlayWav(ctx, buf)
|
||||
|
||||
// 检查是否被取消
|
||||
if ctx.Err() != nil {
|
||||
zap.S().Debugf("[TTS] 播放被取消: %s", text)
|
||||
return
|
||||
}
|
||||
|
||||
zap.S().Infof("[TTS] 播放完成: %s", text)
|
||||
}
|
||||
|
||||
// getToken 获取阿里云 TTS Token
|
||||
func (tts *AliTTS) getToken() error {
|
||||
// Token 未过期则复用
|
||||
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)
|
||||
}
|
||||
|
||||
if resultMessage.ErrMsg != "" {
|
||||
zap.S().Errorf("[TTS] 获取Token失败: %s", resultMessage.ErrMsg)
|
||||
return errorsx.ThirdPartyErr
|
||||
}
|
||||
|
||||
tts.tokenResult = resultMessage.TokenResult
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tts *AliTTS) Get(text string) (io.Reader, error) {
|
||||
// Get 合成语音文本(内部方法)
|
||||
func (tts *AliTTS) Get(ctx context.Context, 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 {
|
||||
if err := tts.getToken(); 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)
|
||||
@@ -124,34 +135,32 @@ func (tts *AliTTS) Get(text string) (io.Reader, error) {
|
||||
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
|
||||
if !done {
|
||||
zap.S().Error("[TTS] 合成失败: done=false")
|
||||
return ttsData.Data, errorsx.ThirdPartyErr
|
||||
}
|
||||
size := ttsData.Data.(*bytes.Buffer).Len()
|
||||
if size == 0 {
|
||||
zap.S().Error("[TTS] 合成数据为空")
|
||||
return ttsData.Data, errorsx.ThirdPartyErr
|
||||
}
|
||||
return ttsData.Data, nil
|
||||
|
||||
case <-time.After(time.Duration(tts.Timeout) * time.Second):
|
||||
zap.S().Errorln("TTS合成超时")
|
||||
zap.S().Errorf("[TTS] 合成超时: %s", text)
|
||||
return ttsData.Data, errorsx.DriverTimeoutErr
|
||||
case <-tts.ctx.Done():
|
||||
zap.S().Errorln("TTS合成被取消")
|
||||
|
||||
case <-ctx.Done():
|
||||
return ttsData.Data, errorsx.DriverCancelErr
|
||||
}
|
||||
}
|
||||
|
||||
func New(ctx context.Context, config config.AliyunConfig) *AliTTS {
|
||||
// New 创建 TTS 实例
|
||||
func New(config config.AliyunConfig) *AliTTS {
|
||||
return &AliTTS{
|
||||
ctx: ctx,
|
||||
AliyunConfig: config,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user