edgetts.go
1 package localtts 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "krillin-ai/internal/storage" 8 "krillin-ai/log" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "time" 14 15 "go.uber.org/zap" 16 ) 17 18 type EdgeTtsClient struct { 19 } 20 21 func NewEdgeTtsClient() *EdgeTtsClient { 22 return &EdgeTtsClient{} 23 } 24 25 func (c *EdgeTtsClient) Text2Speech(text, voice, outputFile string) error { 26 // 清理语音名称中的额外空格 27 voice = strings.TrimSpace(voice) 28 29 // 确保输出目录存在 30 outputDir := filepath.Dir(outputFile) 31 if err := os.MkdirAll(outputDir, 0755); err != nil { 32 log.GetLogger().Error("创建输出目录失败", zap.String("dir", outputDir), zap.Error(err)) 33 return fmt.Errorf("创建输出目录失败: %w", err) 34 } 35 36 // 获取绝对路径 37 absOutputFile, err := filepath.Abs(outputFile) 38 if err != nil { 39 log.GetLogger().Error("获取输出文件绝对路径失败", zap.Error(err)) 40 return fmt.Errorf("获取输出文件绝对路径失败: %w", err) 41 } 42 absOutputDir := filepath.Dir(absOutputFile) 43 44 // 创建临时文件来存储文本内容,避免命令行参数转义问题 45 tempFile, err := ioutil.TempFile(absOutputDir, "edge_tts_text_*.txt") 46 if err != nil { 47 log.GetLogger().Error("创建临时文件失败", zap.Error(err)) 48 return fmt.Errorf("创建临时文件失败: %w", err) 49 } 50 tempFileName := tempFile.Name() 51 52 // 确保在函数结束时清理临时文件 53 defer func() { 54 tempFile.Close() 55 if err := os.Remove(tempFileName); err != nil { 56 log.GetLogger().Warn("清理临时文件失败", zap.String("file", tempFileName), zap.Error(err)) 57 } 58 }() 59 60 // 将文本写入临时文件 61 if _, err := tempFile.WriteString(text); err != nil { 62 log.GetLogger().Error("写入临时文件失败", zap.Error(err)) 63 return fmt.Errorf("写入临时文件失败: %w", err) 64 } 65 tempFile.Close() // 确保文件被写入 66 67 // 重试机制 68 maxRetries := 3 69 for attempt := 1; attempt <= maxRetries; attempt++ { 70 log.GetLogger().Info("edge-tts转录尝试", 71 zap.Int("attempt", attempt), 72 zap.Int("maxRetries", maxRetries), 73 zap.String("text_length", fmt.Sprintf("%d", len(text)))) 74 75 err := c.attemptTTS(tempFileName, voice, absOutputFile, attempt) 76 if err == nil { 77 // 成功生成 78 log.GetLogger().Info("edge-tts转录完成", zap.String("output file", absOutputFile)) 79 if _, err := os.Stat(absOutputFile); os.IsNotExist(err) { 80 log.GetLogger().Error("edge-tts 输出文件不存在", zap.String("output file", absOutputFile)) 81 return fmt.Errorf("edge-tts 输出文件不存在: %s", absOutputFile) 82 } 83 return nil 84 } 85 86 log.GetLogger().Warn("edge-tts转录失败,准备重试", 87 zap.Int("attempt", attempt), 88 zap.Error(err)) 89 90 // 如果不是最后一次尝试,等待一段时间再重试 91 if attempt < maxRetries { 92 waitTime := time.Duration(attempt) * 2 * time.Second 93 log.GetLogger().Info("等待重试", zap.Duration("waitTime", waitTime)) 94 time.Sleep(waitTime) 95 } 96 } 97 98 return fmt.Errorf("edge-tts转录失败,已重试%d次", maxRetries) 99 } 100 101 func (c *EdgeTtsClient) attemptTTS(tempFileName, voice, absOutputFile string, attempt int) error { 102 // 使用新的edge-tts命令参数(文件输入方式) 103 cmdArgs := []string{ 104 "--text-file", tempFileName, 105 "--voice", voice, 106 "--output", absOutputFile, 107 "--format", "wav", 108 "--sample_rate", "44100", 109 } 110 111 // 创建带超时的上下文 112 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // 60秒超时 113 defer cancel() 114 115 cmd := exec.CommandContext(ctx, storage.EdgeTtsPath, cmdArgs...) 116 log.GetLogger().Info("edge-tts转录开始", 117 zap.String("cmd", cmd.String()), 118 zap.String("temp_file", tempFileName), 119 zap.String("output_file", absOutputFile), 120 zap.Int("attempt", attempt)) 121 122 output, err := cmd.CombinedOutput() 123 if err != nil { 124 if ctx.Err() == context.DeadlineExceeded { 125 log.GetLogger().Error("edge-tts cmd 超时", zap.String("output", string(output)), zap.Error(err)) 126 return fmt.Errorf("edge-tts 执行超时") 127 } 128 log.GetLogger().Error("edge-tts cmd 执行失败", zap.String("output", string(output)), zap.Error(err)) 129 return err 130 } 131 132 return nil 133 }