108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package main
|
||
|
||
// gentss:离线生成 TTS 朗读音频(Edge TTS 神经网络语音)并回填各 audio 字段。
|
||
// 用法:
|
||
//
|
||
// go run ./cmd/gentss --list # 打印待生成清单
|
||
// go run ./cmd/gentss --strategy=1 # 只生成第 1 计(8 节点剧本已导入)
|
||
// go run ./cmd/gentss --only=script|content|scene|option|teach # 只生成指定内容源
|
||
// go run ./cmd/gentss --force # 覆盖已生成 mp3 重新合成
|
||
//
|
||
// 幂等:mp3 文件已存在且字段已回填 → 跳过(--force 覆盖);文件在但字段空 → 只补回填。
|
||
// 配置在 config.yml 的 tts 段;产物落 ui-src/static/generated/audio/,URL /static/generated/audio/...(与 PNG 素材同挂载)。
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"sync"
|
||
"time"
|
||
|
||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/glog"
|
||
)
|
||
|
||
var staticDir = "ui-src/static/generated" // 打包入客户端的音频目录
|
||
|
||
type genFlags struct {
|
||
listOnly bool
|
||
only string // script | content | scene | option | teach
|
||
strategy int // 0 = 全部计策
|
||
force bool
|
||
}
|
||
|
||
type ttsConfig struct {
|
||
voice string
|
||
rate string
|
||
version string
|
||
concurrency int
|
||
timeout int
|
||
retries int
|
||
}
|
||
|
||
func main() {
|
||
fl := parseFlags()
|
||
cfg := loadTTSConfig()
|
||
if fl.listOnly {
|
||
printInventory(fl)
|
||
return
|
||
}
|
||
client := &ttsClient{
|
||
voice: cfg.voice,
|
||
rate: cfg.rate,
|
||
version: cfg.version,
|
||
timeout: time.Duration(cfg.timeout) * time.Second,
|
||
retries: cfg.retries,
|
||
}
|
||
tasks := enumerateTasks(fl)
|
||
if len(tasks) == 0 {
|
||
fmt.Println("[gentss] 无待生成任务")
|
||
return
|
||
}
|
||
sem := make(chan struct{}, cfg.concurrency)
|
||
var wg sync.WaitGroup
|
||
for _, t := range tasks {
|
||
t.force = fl.force
|
||
wg.Add(1)
|
||
sem <- struct{}{}
|
||
go func(t task) {
|
||
defer wg.Done()
|
||
defer func() { <-sem }()
|
||
if err := runTask(client, t); err != nil {
|
||
glog.Errorf(ctx, "[gentss] %s 失败: %v", taskDesc(t), err)
|
||
}
|
||
}(t)
|
||
}
|
||
wg.Wait()
|
||
fmt.Println("[gentss] 完成")
|
||
}
|
||
|
||
func parseFlags() genFlags {
|
||
var fl genFlags
|
||
flag.BoolVar(&fl.listOnly, "list", false, "只打印待生成清单")
|
||
flag.StringVar(&fl.only, "only", "", "script | content | scene | option | teach,默认全部")
|
||
flag.IntVar(&fl.strategy, "strategy", 0, "只处理指定计策(strategy_id),0=全部")
|
||
flag.BoolVar(&fl.force, "force", false, "覆盖已生成音频重新合成")
|
||
flag.Parse()
|
||
return fl
|
||
}
|
||
|
||
func loadTTSConfig() ttsConfig {
|
||
return ttsConfig{
|
||
voice: g.Cfg().MustGet(ctx, "tts.voice", "zh-CN-XiaoxiaoNeural").String(),
|
||
rate: g.Cfg().MustGet(ctx, "tts.rate", "+0%").String(),
|
||
version: g.Cfg().MustGet(ctx, "tts.version", "1-143.0.3650.75").String(),
|
||
concurrency: g.Cfg().MustGet(ctx, "tts.concurrency", 2).Int(),
|
||
timeout: g.Cfg().MustGet(ctx, "tts.timeout", 60).Int(),
|
||
retries: g.Cfg().MustGet(ctx, "tts.retries", 3).Int(),
|
||
}
|
||
}
|
||
|
||
func taskDesc(t task) string {
|
||
if t.kind == "script" {
|
||
return fmt.Sprintf("node %d", t.nodeID)
|
||
}
|
||
return t.relPath
|
||
}
|