diff --git a/digitalhuman/Dockerfile b/digitalhuman/Dockerfile new file mode 100644 index 0000000..84a600c --- /dev/null +++ b/digitalhuman/Dockerfile @@ -0,0 +1,49 @@ +# 阶段1: 构建 +FROM golang:1.26-alpine AS builder + +RUN apk add --no-cache git ca-certificates tzdata + +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +ENV GO111MODULE=on +ENV GOPROXY=https://goproxy.cn,direct +ENV CGO_ENABLED=0 +ENV GOTOOLCHAIN=auto +ENV GOPRIVATE=gitea.com/red-future/common + +# 配置git使用私有Gitea仓库 +RUN git config --global url."http://x-token-auth:9b31146aa8c10a7cb4f2e49dcee0934a223be1076289810e1ad98b968066c2bc@116.204.74.41:3000/red-future/common.git".insteadOf "https://gitea.com/red-future/common.git" && \ + git config --global credential.helper store + +WORKDIR /build + +COPY . . + +RUN go mod download && go mod tidy + +RUN go build -ldflags="-s -w" -o main ./main.go + +# 阶段2: 运行 +FROM alpine:3.19 + +RUN apk add --no-cache ca-certificates tzdata + +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /app + +COPY --from=builder /build/main . +COPY --from=builder /build/config.yml ./ + +RUN mkdir -p /app/resource/log/run \ + /app/resource/log/server \ + && adduser -D -u 1000 appuser \ + && chown -R appuser:appuser /app + +USER appuser + +EXPOSE 3006 + +CMD ["./main"] diff --git a/digitalhuman/cmd/main.go b/digitalhuman/cmd/main.go new file mode 100644 index 0000000..d8880fb --- /dev/null +++ b/digitalhuman/cmd/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +var text = "欢迎使用红动未来数字人服务平台,我们将为您提供最优质的AI数字人解决方案。" + +type TTSCommonResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Text string `json:"text"` + Audio string `json:"audio"` +} + +func main() { + // 获取当前工作目录 + outputDir, err := os.Getwd() + if err != nil { + fmt.Printf("获取当前目录失败: %v\n", err) + os.Exit(1) + } + + // 查找项目根目录(向上查找包含 go.mod 的目录) + outputDir = findProjectRoot(outputDir) + + // 验证根目录是否正确(检查是否有 go.mod) + if _, err := os.Stat(outputDir + "/go.mod"); err != nil { + fmt.Printf("未找到项目根目录,当前目录: %s\n", outputDir) + os.Exit(1) + } + + fmt.Println("=================== TTS测试开始 ===================") + fmt.Printf("输出目录: %s\n", outputDir) + fmt.Printf("随机文本: %s\n", text) + fmt.Printf("请求URL: http://127.0.0.1:8000/tts\n") + + // 创建带超时的 HTTP 客户端(120秒超时) + client := &http.Client{ + Timeout: 120 * time.Second, + } + + resp, err := client.Post("http://127.0.0.1:8000/tts", "application/json", bytes.NewBufferString(fmt.Sprintf(`"%s"`, text))) + if err != nil { + fmt.Printf("请求失败: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + // 打印响应头 + fmt.Printf("Content-Type: %s\n", resp.Header.Get("Content-Type")) + fmt.Printf("Content-Length: %s\n", resp.Header.Get("Content-Length")) + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取响应失败: %v\n", err) + os.Exit(1) + } + + fmt.Printf("状态码: %d, 响应大小: %d字节\n", resp.StatusCode, len(body)) + + // 打印响应内容的前200字节(用于调试) + if len(body) > 0 { + previewLen := minInt(200, len(body)) + fmt.Printf("响应内容预览(前%d字节): ", previewLen) + if len(body) >= 4 && string(body[:4]) == "RIFF" { + // WAV文件头 + fmt.Printf("WAV文件格式 (RIFF...)\n") + } else if len(body) >= 3 && string(body[:3]) == "ID3" { + // MP3 ID3标签 + fmt.Printf("MP3 ID3格式\n") + } else if len(body) >= 2 && body[0] == 0xFF && (body[1]&0xE0) == 0xE0 { + // MP3帧同步 + fmt.Printf("MP3帧格式\n") + } else { + // 可能是JSON或其他格式 + fmt.Printf("%s\n", string(body[:previewLen])) + } + } else { + fmt.Printf("响应内容为空!\n") + os.Exit(1) + } + + // 尝试解析JSON响应(包含base64音频) + var commonResp TTSCommonResponse + var audioData []byte + var ext string + + if json.Unmarshal(body, &commonResp) == nil && commonResp.Audio != "" && commonResp.Audio != "base64_placeholder" { + fmt.Printf("检测到JSON响应,code=%d, msg=%s\n", commonResp.Code, commonResp.Msg) + fmt.Printf("Audio字段长度: %d 字符\n", len(commonResp.Audio)) + + // 检查是否成功 + if commonResp.Code != 0 { + fmt.Printf("TTS服务返回错误: %s\n", commonResp.Msg) + os.Exit(1) + } + + // 解码base64音频数据 + decoded, err := base64.StdEncoding.DecodeString(commonResp.Audio) + if err != nil { + fmt.Printf("base64解码失败: %v\n", err) + os.Exit(1) + } + if len(decoded) == 0 { + fmt.Printf("解码后数据为空!\n") + os.Exit(1) + } + + audioData = decoded + fmt.Printf("解码后音频数据大小: %d 字节\n", len(audioData)) + + // 根据解码后的音频数据格式决定扩展名 + if len(audioData) >= 4 && string(audioData[:4]) == "RIFF" { + ext = ".wav" + fmt.Printf("检测到WAV格式\n") + } else if len(audioData) >= 3 && string(audioData[:3]) == "ID3" || (len(audioData) >= 2 && audioData[0] == 0xFF && (audioData[1]&0xE0) == 0xE0) { + ext = ".mp3" + fmt.Printf("检测到MP3格式\n") + } else { + ext = ".wav" // 默认wav + fmt.Printf("未知格式,默认保存为 .wav\n") + } + } else { + // 直接是二进制音频数据 + audioData = body + + // 根据音频数据格式决定扩展名 + if len(audioData) >= 4 && string(audioData[:4]) == "RIFF" { + ext = ".wav" + } else if len(audioData) >= 3 && string(audioData[:3]) == "ID3" || (len(audioData) >= 2 && audioData[0] == 0xFF && (audioData[1]&0xE0) == 0xE0) { + ext = ".mp3" + } else { + ext = ".wav" // 默认wav + } + } + + // 保存音频文件 + filename := fmt.Sprintf("%s/tts_output_%d%s", outputDir, time.Now().Unix(), ext) + if err = os.WriteFile(filename, audioData, 0644); err != nil { + fmt.Printf("写文件失败: %v\n", err) + os.Exit(1) + } + fmt.Printf("音频已保存: %s (%d字节)\n", filename, len(audioData)) + fmt.Println("=================== TTS测试成功 ===================") +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// findProjectRoot 查找项目根目录(包含 go.mod 的目录) +func findProjectRoot(startDir string) string { + dir := startDir + for { + // 检查当前目录是否有 go.mod + if _, err := os.Stat(dir + "/go.mod"); err == nil { + return dir + } + + // 如果已经是根目录或无法继续向上查找,返回当前目录 + parentDir := dir[:maxInt(0, len(dir)-len("/"+getLastPathSegment(dir)))] + if parentDir == dir || parentDir == "" { + return startDir + } + + dir = parentDir + } +} + +// getLastPathSegment 获取路径的最后一部分 +func getLastPathSegment(path string) string { + if idx := strings.LastIndex(path, "/"); idx != -1 { + return path[idx+1:] + } + return path +} diff --git a/digitalhuman/config.yml b/digitalhuman/config.yml new file mode 100644 index 0000000..7cb0d52 --- /dev/null +++ b/digitalhuman/config.yml @@ -0,0 +1,42 @@ +server: + address: ":3006" + name: "digital-human" + workerId: 1 # 雪花算法worker ID +database: + default: + - type: "pgsql" + host: "116.204.74.41" + port: "15432" + user: "postgres" + pass: "Bjang09@686^*^" + name: "digital-human" + prefix: "digital_human_" # (可选)表名前缀 + role: "master" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。 + debug: true # (可选)开启调试模式 + dryRun: false # (可选)ORM空跑(只读不写) + charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。 + timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local + maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10) + maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制) + maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒) + maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。 + createdAt: "created_at" # (可选)自动创建时间字段名称 + updatedAt: "updated_at" # (可选)自动更新时间字段名称 + deletedAt: "deleted_at" # (可选)软删除时间字段名称 + timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效 + +#192.168.3.30 + +redis: + default: + address: "116.204.74.41:6379" + db: 0 + +consul: + address: 116.204.74.41:8500 + +jaeger: + addr: 116.204.74.41:4318 + +model-asynch: + addr: "127.0.0.1:8001" \ No newline at end of file diff --git a/digitalhuman/consts/custom_voice_status.go b/digitalhuman/consts/custom_voice_status.go new file mode 100644 index 0000000..7e995be --- /dev/null +++ b/digitalhuman/consts/custom_voice_status.go @@ -0,0 +1,11 @@ +package consts + +// CustomVoiceStatus 自定义音色状态类型 +type CustomVoiceStatus int + +const ( + CustomVoiceStatusGenerating CustomVoiceStatus = 0 // 生成中 + CustomVoiceStatusSuccess CustomVoiceStatus = 1 // 成功 + CustomVoiceStatusFailed CustomVoiceStatus = 2 // 失败 +) + diff --git a/digitalhuman/consts/public/model_name.go b/digitalhuman/consts/public/model_name.go new file mode 100644 index 0000000..b0afb6c --- /dev/null +++ b/digitalhuman/consts/public/model_name.go @@ -0,0 +1,7 @@ +package public + +const ( + ModelNameCustomVoice = "qwen3-tts-customvoice" // 预设音频 + ModelNameVoiceDesign = "qwen3-tts-voicedesign" // 设计音频 + ModelNameBase = "qwen3-tts-base" // 克隆音频 +) diff --git a/digitalhuman/consts/public/table_name.go b/digitalhuman/consts/public/table_name.go index 116baef..d716e44 100644 --- a/digitalhuman/consts/public/table_name.go +++ b/digitalhuman/consts/public/table_name.go @@ -3,6 +3,7 @@ package public const ( TableNameAudio = "digital_human_audio" TableNameCustomVoice = "digital_human_custom_voice" + TableNameAsyncTaskRef = "digital_human_async_task_ref" // 异步任务绑定表(task_id -> 业务表+业务ID) TableNameVideo = "digital_human_video" TableNameDigitalHuman = "digital_human" ) diff --git a/digitalhuman/controller/async_task_controller.go b/digitalhuman/controller/async_task_controller.go new file mode 100644 index 0000000..36da69e --- /dev/null +++ b/digitalhuman/controller/async_task_controller.go @@ -0,0 +1,31 @@ +package controller + +import ( + "context" + + "digital-human/model/dto" + "digital-human/service" + + "github.com/gogf/gf/v2/util/gconv" +) + +type asyncTask struct{} + +// AsyncTask 异步任务同步控制器(供定时任务服务调用) +var AsyncTask = new(asyncTask) + +// SyncAsyncTasks 扫描待处理任务并同步状态/转移结果 +func (c *asyncTask) SyncAsyncTasks(ctx context.Context, req *dto.SyncAsyncTasksReq) (res *dto.SyncAsyncTasksRes, err error) { + // 从上下文获取用户信息(gfdb Hook 会自动填充) + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.AsyncTask.Sync(ctx, req) +} + diff --git a/digitalhuman/controller/audio_controller.go b/digitalhuman/controller/audio_controller.go new file mode 100644 index 0000000..6c232df --- /dev/null +++ b/digitalhuman/controller/audio_controller.go @@ -0,0 +1,113 @@ +package controller + +import ( + "context" + "digital-human/model/dto" + "digital-human/service" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/util/gconv" +) + +type audio struct{} + +// Audio 音频控制器 +var Audio = new(audio) + +// CreateAudio 创建音频 +func (c *audio) CreateAudio(ctx context.Context, req *dto.CreateAudioReq) (res *dto.CreateAudioRes, err error) { + // 从上下文获取用户信息(gfdb Hook 会自动填充) + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.Audio.Create(ctx, req) +} + +// ListAudio 获取音频列表 +func (c *audio) ListAudio(ctx context.Context, req *dto.ListAudioReq) (res *dto.ListAudioRes, err error) { + // 从上下文获取用户信息 + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.Audio.List(ctx, req) +} + +// GetAudio 获取音频详情 +func (c *audio) GetAudio(ctx context.Context, req *dto.GetAudioReq) (res *dto.GetAudioRes, err error) { + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.Audio.GetOne(ctx, req.ID) +} + +// UpdateAudio 更新音频 +func (c *audio) UpdateAudio(ctx context.Context, req *dto.UpdateAudioReq) (res *beans.ResponseEmpty, err error) { + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + err = service.Audio.Update(ctx, req) + return +} + +// DeleteAudio 删除音频 +func (c *audio) DeleteAudio(ctx context.Context, req *dto.DeleteAudioReq) (res *beans.ResponseEmpty, err error) { + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + err = service.Audio.Delete(ctx, req.ID) + return +} + +// GenerateAudio 重新生成音频 +func (c *audio) GenerateAudio(ctx context.Context, req *dto.GenerateAudioReq) (res *dto.GenerateAudioRes, err error) { + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.Audio.Generate(ctx, req) +} + +// TTS 文本转语音 +func (c *audio) TTS(ctx context.Context, req *dto.TTSReq) (res *dto.TTSRes, err error) { + return service.Audio.TTS(ctx, req) +} + +// GetStatusOptions 获取状态选项 +func (c *audio) GetStatusOptions(ctx context.Context, req *dto.GetAudioStatusOptionsReq) (res *dto.GetAudioStatusOptionsRes, err error) { + return service.Audio.GetStatusOptions(ctx, req) +} diff --git a/digitalhuman/controller/custom_voice_controller.go b/digitalhuman/controller/custom_voice_controller.go new file mode 100644 index 0000000..4922f96 --- /dev/null +++ b/digitalhuman/controller/custom_voice_controller.go @@ -0,0 +1,61 @@ +package controller + +import ( + "context" + "digital-human/model/dto" + "digital-human/service" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/util/gconv" +) + +type customVoice struct{} + +// CustomVoice 自定义音色控制器 +var CustomVoice = new(customVoice) + +// CreateCustomVoice 创建自定义音色 +func (c *customVoice) CreateCustomVoice(ctx context.Context, req *dto.CreateCustomVoiceReq) (res *dto.CreateCustomVoiceRes, err error) { + // 从上下文获取用户信息 + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.CustomVoice.CreateCustomVoice(ctx, req) +} + +// ListCustomVoices 获取自定义音色列表 +func (c *customVoice) ListCustomVoices(ctx context.Context, req *dto.ListCustomVoiceReq) (res *dto.ListCustomVoiceRes, err error) { + // 从上下文获取用户信息 + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + return service.CustomVoice.ListCustomVoices(ctx, req) +} + +// DeleteCustomVoice 删除自定义音色 +func (c *customVoice) DeleteCustomVoice(ctx context.Context, req *dto.DeleteCustomVoiceReq) (res *beans.ResponseEmpty, err error) { + // 从上下文获取用户信息 + if ctx.Value("userId") == nil { + ctx = context.WithValue(ctx, "userId", gconv.String(1)) + } + if ctx.Value("userName") == nil { + ctx = context.WithValue(ctx, "userName", "admin") + } + if ctx.Value("tenantId") == nil { + ctx = context.WithValue(ctx, "tenantId", uint64(1)) + } + err = service.CustomVoice.DeleteCustomVoice(ctx, req) + return +} diff --git a/digitalhuman/controller/digitalhuman_controller.go b/digitalhuman/controller/digitalhuman_controller.go new file mode 100644 index 0000000..bbbf691 --- /dev/null +++ b/digitalhuman/controller/digitalhuman_controller.go @@ -0,0 +1,67 @@ +package controller + +import ( + "context" + "digital-human/model/dto" + "digital-human/service" + + "gitea.com/red-future/common/beans" +) + +type digitalhuman struct{} + +// DigitalHuman 数字人形象控制器 +var DigitalHuman = new(digitalhuman) + +// CreateDigitalHuman 创建数字人形象 +func (c *digitalhuman) CreateDigitalHuman(ctx context.Context, req *dto.CreateDigitalHumanReq) (res *dto.CreateDigitalHumanRes, err error) { + return service.DigitalHuman.Create(ctx, req) +} + +// ListDigitalHuman 获取数字人形象列表 +func (c *digitalhuman) ListDigitalHuman(ctx context.Context, req *dto.ListDigitalHumanReq) (res *dto.ListDigitalHumanRes, err error) { + return service.DigitalHuman.List(ctx, req) +} + +// GetDigitalHuman 获取数字人形象详情 +func (c *digitalhuman) GetDigitalHuman(ctx context.Context, req *dto.GetDigitalHumanReq) (res *dto.GetDigitalHumanRes, err error) { + return service.DigitalHuman.GetOne(ctx, req.ID) +} + +// UpdateDigitalHuman 更新数字人形象 +func (c *digitalhuman) UpdateDigitalHuman(ctx context.Context, req *dto.UpdateDigitalHumanReq) (res *beans.ResponseEmpty, err error) { + err = service.DigitalHuman.Update(ctx, req) + return +} + +// UpdateDigitalHumanStatus 更新数字人形象状态 +func (c *digitalhuman) UpdateDigitalHumanStatus(ctx context.Context, req *dto.UpdateDigitalHumanStatusReq) (res *beans.ResponseEmpty, err error) { + err = service.DigitalHuman.UpdateStatus(ctx, req.ID, req.Status) + return +} + +// DeleteDigitalHuman 删除数字人形象 +func (c *digitalhuman) DeleteDigitalHuman(ctx context.Context, req *dto.DeleteDigitalHumanReq) (res *beans.ResponseEmpty, err error) { + err = service.DigitalHuman.Delete(ctx, req.ID) + return +} + +// GetDigitalHumanStatusOptions 获取数字人状态选项 +func (c *digitalhuman) GetDigitalHumanStatusOptions(ctx context.Context, req *dto.GetDigitalHumanStatusOptionsReq) (res *dto.GetDigitalHumanStatusOptionsRes, err error) { + return service.DigitalHuman.GetStatusOptions(ctx, req) +} + +// GetGenderOptions 获取性别选项 +func (c *digitalhuman) GetGenderOptions(ctx context.Context, req *dto.GetGenderOptionsReq) (res *dto.GetGenderOptionsRes, err error) { + return service.DigitalHuman.GetGenderOptions(ctx, req) +} + +// GetAgeOptions 获取年龄段选项 +func (c *digitalhuman) GetAgeOptions(ctx context.Context, req *dto.GetAgeOptionsReq) (res *dto.GetAgeOptionsRes, err error) { + return service.DigitalHuman.GetAgeOptions(ctx, req) +} + +// GetStyleOptions 获取风格选项 +func (c *digitalhuman) GetStyleOptions(ctx context.Context, req *dto.GetStyleOptionsReq) (res *dto.GetStyleOptionsRes, err error) { + return service.DigitalHuman.GetStyleOptions(ctx, req) +} diff --git a/digitalhuman/controller/video_controller.go b/digitalhuman/controller/video_controller.go new file mode 100644 index 0000000..e94a197 --- /dev/null +++ b/digitalhuman/controller/video_controller.go @@ -0,0 +1,56 @@ +package controller + +import ( + "context" + "digital-human/model/dto" + "digital-human/service" + + "gitea.com/red-future/common/beans" +) + +type video struct{} + +// Video 视频控制器 +var Video = new(video) + +// CreateVideo 创建视频 +func (c *video) CreateVideo(ctx context.Context, req *dto.CreateVideoReq) (res *dto.CreateVideoRes, err error) { + return service.Video.Create(ctx, req) +} + +// ListVideo 获取视频列表 +func (c *video) ListVideo(ctx context.Context, req *dto.ListVideoReq) (res *dto.ListVideoRes, err error) { + return service.Video.List(ctx, req) +} + +// GetVideo 获取视频详情 +func (c *video) GetVideo(ctx context.Context, req *dto.GetVideoReq) (res *dto.GetVideoRes, err error) { + return service.Video.GetOne(ctx, req.ID) +} + +// UpdateVideo 更新视频 +func (c *video) UpdateVideo(ctx context.Context, req *dto.UpdateVideoReq) (res *beans.ResponseEmpty, err error) { + err = service.Video.Update(ctx, req) + return +} + +// DeleteVideo 删除视频 +func (c *video) DeleteVideo(ctx context.Context, req *dto.DeleteVideoReq) (res *beans.ResponseEmpty, err error) { + err = service.Video.Delete(ctx, req.ID) + return +} + +// GenerateVideo 生成视频 +func (c *video) GenerateVideo(ctx context.Context, req *dto.GenerateVideoReq) (res *dto.GenerateVideoRes, err error) { + return service.Video.Generate(ctx, req) +} + +// GetVideoStatusOptions 获取视频状态选项 +func (c *video) GetVideoStatusOptions(ctx context.Context, req *dto.GetVideoStatusOptionsReq) (res *dto.GetVideoStatusOptionsRes, err error) { + return service.Video.GetStatusOptions(ctx, req) +} + +// GetResolutionOptions 获取分辨率选项 +func (c *video) GetResolutionOptions(ctx context.Context, req *dto.GetResolutionOptionsReq) (res *dto.GetResolutionOptionsRes, err error) { + return service.Video.GetResolutionOptions(ctx, req) +} diff --git a/digitalhuman/dao/async_task_ref_dao.go b/digitalhuman/dao/async_task_ref_dao.go new file mode 100644 index 0000000..7c23503 --- /dev/null +++ b/digitalhuman/dao/async_task_ref_dao.go @@ -0,0 +1,69 @@ +package dao + +import ( + "context" + + "digital-human/consts/public" + "digital-human/model/entity" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" +) + +var AsyncTaskRef = &asyncTaskRefDao{} + +type asyncTaskRefDao struct{} + +func (d *asyncTaskRefDao) Insert(ctx context.Context, ref *entity.AsyncTaskRef) (id int64, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef).Data(ref).Insert() + if err != nil { + return 0, err + } + return r.LastInsertId() +} + +// ListPending 列出待处理任务绑定(state=0/1) +func (d *asyncTaskRefDao) ListPending(ctx context.Context, limit int) (list []*entity.AsyncTaskRef, err error) { + if limit <= 0 { + limit = 200 + } + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef). + Where("deleted_at IS NULL"). + // 业务侧只维护三态:生成中/成功/失败;绑定表仅用于“待同步列表” + WhereIn(entity.AsyncTaskRefCol.State, []int{0, 1}). + OrderAsc(entity.AsyncTaskRefCol.UpdatedAt). + Limit(limit). + All() + if err != nil { + return nil, err + } + err = r.Structs(&list) + return +} + +func (d *asyncTaskRefDao) UpdateByTaskID(ctx context.Context, taskID string, data gdb.Map) (rows int64, err error) { + // 触发 gfdb 的 updateHook 自动填充 updater,需要显式带 updater 字段 + data[entity.AsyncTaskRefCol.Updater] = "" + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef). + Where(entity.AsyncTaskRefCol.TaskID, taskID). + Data(data). + Update() + if err != nil { + return 0, err + } + return r.RowsAffected() +} + +func (d *asyncTaskRefDao) GetByTaskID(ctx context.Context, taskID string) (ref *entity.AsyncTaskRef, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAsyncTaskRef). + Where(entity.AsyncTaskRefCol.TaskID, taskID). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + err = r.Struct(&ref) + return +} diff --git a/digitalhuman/dao/audio_dao.go b/digitalhuman/dao/audio_dao.go new file mode 100644 index 0000000..b6cde6e --- /dev/null +++ b/digitalhuman/dao/audio_dao.go @@ -0,0 +1,128 @@ +package dao + +import ( + "context" + "digital-human/consts" + "digital-human/consts/public" + "digital-human/model/dto" + "digital-human/model/entity" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var Audio = &audio{} + +type audio struct{} + +// Insert 插入音频 +func (d *audio) Insert(ctx context.Context, req *dto.CreateAudioReq) (id int64, err error) { + var res *entity.Audio + if err = gconv.Struct(req, &res); err != nil { + return + } + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Data(&res).Insert() + if err != nil { + return + } + return r.LastInsertId() +} + +// Update 更新音频 +func (d *audio) Update(ctx context.Context, id int64, updateData *entity.Audio) (rows int64, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id).Data(&updateData).Update() + if err != nil { + return + } + return r.RowsAffected() +} + +// UpdateStatus 更新音频状态 +func (d *audio) UpdateStatus(ctx context.Context, id int64, status consts.AudioStatus, errorMsg string, audioURL string, duration int, externalID string) (rows int64, err error) { + model := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id) + + updateData := gdb.Map{ + entity.AudioCol.Status: int(status), + } + if errorMsg != "" { + updateData[entity.AudioCol.ErrorMsg] = errorMsg + } + if audioURL != "" { + updateData[entity.AudioCol.AudioURL] = audioURL + } + if duration > 0 { + updateData[entity.AudioCol.Duration] = duration + } + if externalID != "" { + updateData[entity.AudioCol.ExternalID] = externalID + } + + r, err := model.Data(&updateData).Update() + if err != nil { + return + } + return r.RowsAffected() +} + +// Delete 删除音频 +func (d *audio) Delete(ctx context.Context, id int64) (rows int64, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +// GetOne 获取单个音频 +func (d *audio) GetOne(ctx context.Context, id int64) (audio *entity.Audio, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).Where(entity.AudioCol.Id, id).One() + if err != nil { + return + } + err = r.Struct(&audio) + return +} + +// List 获取音频列表 +func (d *audio) List(ctx context.Context, req *dto.ListAudioReq) (res []*entity.Audio, total int, err error) { + model := gfdb.DB(ctx).Model(ctx, public.TableNameAudio).OmitEmpty() + + // 构建查询过滤条件 + if req.Status != consts.AudioStatusGenerating && req.Status != consts.AudioStatusSuccess && req.Status != consts.AudioStatusFailed { + // 不添加状态过滤 + } else { + model = model.Where(entity.AudioCol.Status+" = ?", req.Status) + } + + if !g.IsEmpty(req.Keyword) { + like := "%" + req.Keyword + "%" + model = model.Where( + "("+entity.AudioCol.Name+ + " LIKE ? OR "+entity.AudioCol.Description+ + " LIKE ? OR "+entity.AudioCol.ScriptText+ + " LIKE ?)", + like, like, like, + ) + } + + model = model.OrderDesc(entity.AudioCol.CreatedAt) + + if req.Page != nil { + if req.Page.PageNum <= 0 { + req.Page.PageNum = 1 + } + if req.Page.PageSize <= 0 { + req.Page.PageSize = 10 + } + model = model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + + r, total, err := model.AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/digitalhuman/dao/custom_voice_dao.go b/digitalhuman/dao/custom_voice_dao.go new file mode 100644 index 0000000..911340e --- /dev/null +++ b/digitalhuman/dao/custom_voice_dao.go @@ -0,0 +1,135 @@ +package dao + +import ( + "context" + "digital-human/consts/public" + "digital-human/model/dto" + "digital-human/model/entity" + + "gitea.com/red-future/common/beans" + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/util/gconv" +) + +// CustomVoice 自定义音色数据访问层 +var CustomVoice = &customVoice{} + +type customVoice struct{} + +// Insert 插入自定义音色 +func (d *customVoice) Insert(ctx context.Context, req *dto.CreateCustomVoiceReq) (id int64, err error) { + var result *entity.CustomVoice + if err = gconv.Struct(req, &result); err != nil { + return + } + // 初始状态:生成中 + result.Status = 0 + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).Data(&result).Insert() + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *customVoice) UpdateReferenceAudio(ctx context.Context, id int64, referenceAudio []byte) (rows int64, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice). + Where(entity.CustomVoiceCol.Id, id). + Data(gdb.Map{ + entity.CustomVoiceCol.ReferenceAudio: referenceAudio, + }). + Update() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *customVoice) UpdateDescription(ctx context.Context, id int64, description string) (rows int64, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice). + Where(entity.CustomVoiceCol.Id, id). + Data(gdb.Map{ + entity.CustomVoiceCol.Description: description, + }). + Update() + if err != nil { + return + } + return r.RowsAffected() +} + +// UpdateStatus 更新自定义音色状态/结果 +func (d *customVoice) UpdateStatus(ctx context.Context, id int64, status int, errorMsg string, ossFile string) (rows int64, err error) { + data := gdb.Map{ + entity.CustomVoiceCol.Status: status, + } + if errorMsg != "" { + data[entity.CustomVoiceCol.ErrorMsg] = errorMsg + } + if ossFile != "" { + data[entity.CustomVoiceCol.OssFile] = ossFile + } + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice). + Where(entity.CustomVoiceCol.Id, id). + Data(data). + Update() + if err != nil { + return 0, err + } + return r.RowsAffected() +} + +// Delete 删除自定义音色 +func (d *customVoice) Delete(ctx context.Context, id int64) (rows int64, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).Where(entity.CustomVoiceCol.Id, id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +// GetOne 获取单个自定义音色 +func (d *customVoice) GetOne(ctx context.Context, id int64) (customVoice *entity.CustomVoice, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice).Where(entity.CustomVoiceCol.Id, id).One() + if err != nil { + return + } + err = r.Struct(&customVoice) + return +} + +// List 获取自定义音色列表 +func (d *customVoice) List(ctx context.Context, req *dto.ListCustomVoiceReq) (res []*entity.CustomVoice, total int, err error) { + model := gfdb.DB(ctx).Model(ctx, public.TableNameCustomVoice) + + // 处理分页 + if req.Page == nil { + req.Page = &beans.Page{PageNum: 1, PageSize: 20} + } + + r, total, err := model.OrderDesc(entity.CustomVoiceCol.CreatedAt).Page(int(req.Page.PageNum), int(req.Page.PageSize)).AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} + +// GetCustomVoiceItem 转换为 DTO 列表项 +func (d *customVoice) GetCustomVoiceItem(entity *entity.CustomVoice) *dto.CustomVoiceItem { + item := &dto.CustomVoiceItem{ + ID: gconv.String(entity.Id), + Name: entity.Name, + Description: entity.Description, + Status: entity.Status, + ErrorMsg: entity.ErrorMsg, + OssFile: entity.OssFile, + } + if entity.CreatedAt != nil { + item.CreatedAt = entity.CreatedAt + } + if entity.UpdatedAt != nil { + item.UpdatedAt = entity.UpdatedAt + } + return item +} diff --git a/digitalhuman/dao/digitalhuman_dao.go b/digitalhuman/dao/digitalhuman_dao.go new file mode 100644 index 0000000..9ed5c86 --- /dev/null +++ b/digitalhuman/dao/digitalhuman_dao.go @@ -0,0 +1,125 @@ +package dao + +import ( + "context" + "digital-human/consts" + "digital-human/consts/public" + "digital-human/model/dto" + "digital-human/model/entity" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +// DigitalHuman 数字人形象数据 +var DigitalHuman = &digitalHuman{} + +type digitalHuman struct{} + +// Insert 插入数字人形象 +func (d *digitalHuman) Insert(ctx context.Context, req *dto.CreateDigitalHumanReq) (ids []any, err error) { + var result entity.DigitalHuman + if err = gconv.Struct(req, &result); err != nil { + return + } + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Data(&result).Insert() + if err != nil { + return + } + lastInsertId, err := r.LastInsertId() + if err != nil { + return + } + ids = []any{lastInsertId} + return +} + +// Update 更新数字人形象 +func (d *digitalHuman) Update(ctx context.Context, id int64, updateData *entity.DigitalHuman) (err error) { + _, err = gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Data(updateData).Where("id = ?", id).Update() + return +} + +// UpdateStatus 更新数字人形象状态 +func (d *digitalHuman) UpdateStatus(ctx context.Context, id int64, status consts.DigitalHumanStatus) (rows int64, err error) { + model := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("id = ?", id) + r, err := model.Data(g.Map{"status": status}).Update() + if err != nil { + return + } + return r.RowsAffected() +} + +// Delete 删除数字人形象 +func (d *digitalHuman) Delete(ctx context.Context, id int64) (err error) { + _, err = gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("id = ?", id).Delete() + return +} + +// GetOne 获取单个数字人形象 +func (d *digitalHuman) GetOne(ctx context.Context, id int64) (digitalHuman *entity.DigitalHuman, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("id = ?", id).One() + if err != nil { + return + } + err = r.Struct(&digitalHuman) + return +} + +// List 获取数字人形象列表 +func (d *digitalHuman) List(ctx context.Context, req *dto.ListDigitalHumanReq) (res []entity.DigitalHuman, total int64, err error) { + model := d.buildListFilter(ctx, req) + + var totalCount int + totalCount, err = model.Count() + if err != nil { + return + } + total = gconv.Int64(totalCount) + + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + + r, err := model.OrderDesc("id").All() + if err != nil { + return + } + err = r.Structs(&res) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *digitalHuman) buildListFilter(ctx context.Context, req *dto.ListDigitalHumanReq) *gdb.Model { + model := gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).OmitEmpty() + // 状态字段允许查询所有状态值,包括0(停用),所以需要特别处理 + if req.Status != consts.DigitalHumanStatusInactive && req.Status != consts.DigitalHumanStatusActive { + // 如果状态不是有效值之一,则不添加状态过滤条件 + } else { + model = model.Where("status = ?", req.Status) + } + if !g.IsEmpty(req.Gender) { + model = model.Where("gender = ?", req.Gender) + } + if !g.IsEmpty(req.Style) { + model = model.Where("style = ?", req.Style) + } + if !g.IsEmpty(req.Keyword) { + model = model.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%") + } + + return model +} + +// Count 计数 +func (d *digitalHuman) Count(ctx context.Context, req *dto.CreateDigitalHumanReq) (count int64, err error) { + var totalCount int + totalCount, err = gfdb.DB(ctx).Model(ctx, public.TableNameDigitalHuman).Where("name = ?", req.Name).Count() + if err != nil { + return + } + count = gconv.Int64(totalCount) + return +} diff --git a/digitalhuman/dao/video_dao.go b/digitalhuman/dao/video_dao.go new file mode 100644 index 0000000..c2c6e42 --- /dev/null +++ b/digitalhuman/dao/video_dao.go @@ -0,0 +1,131 @@ +package dao + +import ( + "context" + "digital-human/consts" + "digital-human/consts/public" + "digital-human/model/dto" + "digital-human/model/entity" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +// Video 视频数据 +var Video = &video{} + +type video struct{} + +// Insert 插入视频 +func (d *video) Insert(ctx context.Context, req *dto.CreateVideoReq) (ids []any, err error) { + var result entity.Video + if err = gconv.Struct(req, &result); err != nil { + return + } + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Data(&result).Insert() + if err != nil { + return + } + lastInsertId, err := r.LastInsertId() + if err != nil { + return + } + ids = []any{lastInsertId} + return +} + +// Update 更新视频 +func (d *video) Update(ctx context.Context, id int64, updateData *entity.Video) (err error) { + _, err = gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Data(updateData).Where("id = ?", id).Update() + return +} + +// Delete 删除视频 +func (d *video) Delete(ctx context.Context, id int64) (err error) { + _, err = gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Where("id = ?", id).Delete() + return +} + +// GetOne 获取单个视频 +func (d *video) GetOne(ctx context.Context, id int64) (video *entity.Video, err error) { + r, err := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Where("id = ?", id).One() + if err != nil { + return + } + err = r.Struct(&video) + return +} + +// List 获取视频列表 +func (d *video) List(ctx context.Context, req *dto.ListVideoReq) (res []entity.Video, total int64, err error) { + model := d.buildListFilter(ctx, req) + + var totalCount int + totalCount, err = model.Count() + if err != nil { + return + } + total = gconv.Int64(totalCount) + + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + + r, err := model.OrderDesc("id").All() + if err != nil { + return + } + err = r.Structs(&res) + return +} + +// buildListFilter 构建列表查询的过滤条件 +func (d *video) buildListFilter(ctx context.Context, req *dto.ListVideoReq) *gdb.Model { + model := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).OmitEmpty() + // 状态字段允许查询所有状态值,包括0(生成中),所以需要特别处理 + if req.Status != consts.VideoStatusGenerating && req.Status != consts.VideoStatusSuccess && req.Status != consts.VideoStatusFailed { + // 如果状态不是有效值之一,则不添加状态过滤条件 + } else { + model = model.Where("status = ?", req.Status) + } + if !g.IsEmpty(req.DigitalHumanID) { + model = model.Where("digitalHumanId = ?", req.DigitalHumanID) + } + if !g.IsEmpty(req.Keyword) { + model = model.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%") + } + + return model +} + +// UpdateStatus 更新视频状态 +func (d *video) UpdateStatus(ctx context.Context, id int64, status consts.VideoStatus, errorMsg string, videoURL string, duration int, thumbnailURL string, externalTaskID string) (rows int64, err error) { + model := gfdb.DB(ctx).Model(ctx, public.TableNameVideo).Where("id = ?", id) + + updateData := gdb.Map{ + "status": status, + } + if errorMsg != "" { + updateData["errorMsg"] = errorMsg + } + if videoURL != "" { + updateData["videoUrl"] = videoURL + } + if duration > 0 { + updateData["duration"] = duration + } + if thumbnailURL != "" { + updateData["thumbnailUrl"] = thumbnailURL + } + if externalTaskID != "" { + updateData["externalTaskId"] = externalTaskID + } + + r, err := model.Data(updateData).Update() + if err != nil { + return + } + return r.RowsAffected() +} diff --git a/digitalhuman/docker部署模型.md b/digitalhuman/docker部署模型.md new file mode 100644 index 0000000..c3caf1b --- /dev/null +++ b/digitalhuman/docker部署模型.md @@ -0,0 +1,698 @@ +# Qwen3-TTS Docker 快速部署文档 + +## 模型版本选择 + +**当前部署模型:Qwen3-TTS-24Hz-1.7B-Base-VoiceClone** + +这是 Qwen3-TTS 系列中**功能最全面、音质最高**的模型版本,支持高质量声音克隆。 + +### 模型对比表 + +| 模型名称 | 采样率 | 参数量 | 体积 | 音质 | 速度 | 特殊功能 | 适用场景 | +|---------|--------|--------|------|------|------|---------|---------| +| Qwen3-TTS-12Hz-0.6B-CustomVoice | 12kHz | 0.6B | 1.7GB | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 9种预设声音 | 性能优先 | +| Qwen3-TTS-12Hz-1.7B-CustomVoice | 12kHz | 1.7B | ~4GB | ⭐⭐⭐⭐ | ⭐⭐⭐ | 9种预设声音 | 需要更高音质 | +| Qwen3-TTS-24Hz-0.6B-CustomVoice | 24kHz | 0.6B | ~2GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 9种预设声音 | 高质量需求 | +| Qwen3-TTS-24Hz-1.7B-CustomVoice | 24kHz | 1.7B | ~5GB | ⭐⭐⭐⭐⭐ | ⭐⭐ | 9种预设声音 | 最高音质(无克隆) | +| Qwen3-TTS-12Hz-Base-VoiceClone | 12kHz | 0.6B | ~2GB | ⭐⭐⭐ | ⭐⭐⭐⭐ | 声音克隆 | 自定义声音 | +| **Qwen3-TTS-24Hz-1.7B-Base-VoiceClone** | **24kHz** | **1.7B** | **~6.8GB** | **⭐⭐⭐⭐⭐** | **⭐⭐** | **声音克隆** | **功能最全面** | + +**当前模型特点:** +- **24kHz 采样率**:双倍于 12Hz 模型,音质更清晰自然 +- **1.7B 参数**:模型表达能力最强 +- **声音克隆**:支持自定义声音训练和生成 +- **功能最全面**:兼具基础模型 + 声音克隆功能 + +**推荐方案:** +- **默认当前选择**:`Qwen3-TTS-24Hz-1.7B-Base-VoiceClone`(功能最全面 + 最高音质) +- **性能优先**:`Qwen3-TTS-12Hz-0.6B-CustomVoice` +- **高质量(无克隆)**:`Qwen3-TTS-24Hz-1.7B-CustomVoice` + +--- + +## 快速开始(3 步部署) + +### 步骤 1:下载模型 + +```bash +# 进入工作目录 +cd ~/Qwen3-TTS +mkdir -p model +cd model + +# 安装 ModelScope CLI(国内用户推荐) +python3 -m pip install -U modelscope + +# 下载 Tokenizer(约 651MB) +modelscope download --model Qwen/Qwen3-TTS-Tokenizer-24Hz --local_dir ./Qwen3-TTS-Tokenizer-24Hz + +# 下载 Qwen3-TTS-24Hz-1.7B-Base-VoiceClone 模型(约 6.8GB) +# 这是功能最全面、音质最高的模型,支持声音克隆 +modelscope download --model Qwen/Qwen3-TTS-24Hz-1.7B-Base-VoiceClone --local_dir ./Qwen3-TTS-24Hz-1.7B-Base-VoiceClone +``` + +**说明:** +- **ModelScope**:阿里云模型托管平台,国内下载速度快 +- **Tokenizer**:分词器,将文本转换为模型能理解的 token(24Hz 版本) +- **TTS 模型**:核心模型,1.7B 参数 + 24Hz 采样率 + 声音克隆功能 + +### 步骤 2:创建文件 + +```bash +cd ~/Qwen3-TTS +``` + +#### 创建 app.py(已配置为 24Hz-1.7B-Base-VoiceClone 模型) + +```bash +cat > app.py << 'EOF' +from fastapi import FastAPI, Body +import soundfile as sf +import io +import torch +import sys +import base64 + +# 添加路径以支持导入 +sys.path.insert(0, '/app') + +from qwen_tts import Qwen3TTSModel + +# ========== 当前配置:Qwen3-TTS-24Hz-1.7B-Base-VoiceClone ========== +# 功能最全面、音质最高的模型,支持声音克隆 +# - 24kHz 采样率:音质更清晰自然 +# - 1.7B 参数:模型表达能力最强 +# - Base-VoiceClone:支持自定义声音克隆 +MODEL_PATH = '/app/model/Qwen3-TTS-24Hz-1.7B-Base-VoiceClone' +TOKENIZER_PATH = '/app/model/Qwen3-TTS-Tokenizer-24Hz' + +# 如需切换到其他模型,请修改以下路径并重新构建镜像 +# ========================================================== + +app = FastAPI(title="Qwen3-TTS API") + +# 全局模型 +model = None + +@app.on_event("startup") +async def startup_event(): + global model + print("正在加载 TTS 模型...") + print(f"模型路径: {MODEL_PATH}") + print(f"模型版本: Qwen3-TTS-24Hz-1.7B-Base-VoiceClone") + print("提示: 1.7B 模型加载需要较长时间,请耐心等待...") + model = Qwen3TTSModel.from_pretrained( + MODEL_PATH, + tokenizer_path=TOKENIZER_PATH, + device_map='cpu', + dtype=torch.float32 + ) + print("TTS 模型初始化完成") + +@app.get("/") +async def root(): + return { + "status": "running", + "mode": "production", + "model": "Qwen3-TTS-24Hz-1.7B-Base-VoiceClone", + "features": ["voice_clone", "high_quality", "24khz"] + } + +@app.post("/tts") +async def tts(text: str = Body(..., media_type='application/json')): + """ + 注意:必须使用 Body(...) 解析请求体,否则会返回 422 错误 + 请求格式: POST /tts,Body 为 JSON 字符串 "文本内容" + + 超时设置:长文本推理可能需要较长时间,建议客户端设置超时时间 > 120 秒 + CPU 推理速度:约 0.5-2 秒/字符(1.7B 模型较慢,比 0.6B 模型慢约 2-3 倍) + """ + if not model: + return {"code": 500, "msg": "模型未初始化"} + + try: + print(f"收到TTS请求,文本长度: {len(text)} 字符") + wavs, sr = model.generate_custom_voice(text, speaker='serena') + print(f"音频生成完成,采样率: {sr}, 音频时长: {len(wavs[0])/sr:.2f}秒") + + # 转换为 WAV 格式 + buffer = io.BytesIO() + sf.write(buffer, wavs[0], sr, format='WAV') + buffer.seek(0) + + audio_data = buffer.read() + audio_b64 = base64.b64encode(audio_data).decode('utf-8') + print(f"编码完成,base64 长度: {len(audio_b64)}") + + return { + "code": 0, + "msg": "success", + "text": text, + "audio": audio_b64 + } + except Exception as e: + import traceback + print(f"Error: {traceback.format_exc()}") + return {"code": 500, "msg": f"TTS处理错误: {str(e)}"} +EOF +``` + +#### 创建 requirements.txt + +```bash +cat > requirements.txt << 'EOF' +fastapi>=0.104.0 +uvicorn>=0.24.0 +numpy>=1.24.0 +torch>=2.0.0 +librosa>=0.10.0 +soundfile>=0.12.0 +safetensors>=0.4.0 +qwen-tts>=0.1.0 +EOF +``` + +#### 创建 Dockerfile + +```bash +cat > Dockerfile << 'EOF' +FROM python:3.12-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + git \ + ffmpeg \ + libsndfile1 \ + && rm -rf /var/lib/apt/lists/* + +# 安装 qwen-tts(包含 qwen_tts 模块) +RUN pip install --no-cache-dir qwen-tts + +# 复制应用文件 +COPY app.py . +COPY requirements.txt . + +# 安装额外依赖 +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 8000 + +# 增加超时设置(180秒,1.7B 模型需要更长时间) +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "180", "--limit-concurrency", "1"] +EOF +``` + +### 步骤 3:构建并启动 + +```bash +cd ~/Qwen3-TTS + +# 构建镜像 +docker build -t qwen3-tts:latest . + +# 启动容器(挂载模型目录,增加内存限制) +docker run -d \ + --name tts-service \ + -p 8000:8000 \ + -v ~/Qwen3-TTS/model:/app/model:ro \ + --memory="8g" \ + --restart unless-stopped \ + qwen3-tts:latest + +# 等待服务启动(1.7B 模型需要更长时间,约15-30秒) +sleep 20 + +# 验证服务 +curl http://localhost:8000/ +``` + +**预期输出:** +```json +{ + "status": "running", + "mode": "production", + "model": "Qwen3-TTS-24Hz-1.7B-Base-VoiceClone", + "features": ["voice_clone", "high_quality", "24khz"] +} +``` + +--- + +## 切换到其他模型版本 + +### 切换到性能优先模型(12Hz-0.6B-CustomVoice) + +```bash +# 1. 下载新模型 +cd ~/Qwen3-TTS/model +modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice +modelscope download --model Qwen/Qwen3-TTS-Tokenizer-12Hz --local_dir ./Qwen3-TTS-Tokenizer-12Hz + +# 2. 修改 app.py 中的 MODEL_PATH 和 TOKENIZER_PATH +# MODEL_PATH = '/app/model/Qwen3-TTS-12Hz-0.6B-CustomVoice' +# TOKENIZER_PATH = '/app/model/Qwen3-TTS-Tokenizer-12Hz' + +# 3. 重新构建并启动 +cd ~/Qwen3-TTS +docker stop tts-service +docker build -t qwen3-tts:v12hz . +docker run -d --name tts-service -p 8000:8000 -v ~/Qwen3-TTS/model:/app/model:ro qwen3-tts:v12hz +``` + +### 切换到最高音质模型(24Hz-1.7B-CustomVoice,无声音克隆) + +```bash +# 1. 下载新模型 +cd ~/Qwen3-TTS/model +modelscope download --model Qwen/Qwen3-TTS-24Hz-1.7B-CustomVoice --local_dir ./Qwen3-TTS-24Hz-1.7B-CustomVoice + +# 2. 修改 app.py 中的路径 +# MODEL_PATH = '/app/model/Qwen3-TTS-24Hz-1.7B-CustomVoice' +# TOKENIZER_PATH = '/app/model/Qwen3-TTS-Tokenizer-24Hz' + +# 3. 重新构建并启动 +cd ~/Qwen3-TTS +docker stop tts-service +docker build -t qwen3-tts:v24hz-noclone . +docker run -d --name tts-service -p 8000:8000 -v ~/Qwen3-TTS/model:/app/model:ro qwen3-tts:v24hz-noclone +``` + +--- + +## API 使用 + +### 健康检查 + +```bash +curl http://localhost:8000/ +``` + +### 文本转语音 + +```bash +curl -X POST http://localhost:8000/tts \ + -H "Content-Type: application/json" \ + -d '"你好,这是一个测试"' +``` + +**注意:** Body 必须是 JSON 字符串格式,不能是 JSON 对象 `{"text": "..."}` + +**响应示例:** +```json +{ + "code": 0, + "msg": "success", + "text": "你好,这是一个测试", + "audio": "UklGRiTCAQBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YQD..." +} +``` + +### 长文本处理 + +**重要提示:** +- 1.7B 模型推理速度较慢(约 1-3 秒/字符) +- 短文本(< 20 字):约 15-30 秒 +- 中等文本(20-50 字):约 50-120 秒 +- 长文本(50-100 字):约 120-240 秒 + +**客户端必须设置超时时间 >= 180 秒**,否则会收到 `EOF` 错误。 + +### Go 调用示例(带超时设置) + +```go +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "time" +) + +type TTSResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Text string `json:"text"` + Audio string `json:"audio"` +} + +func TTS(text string) ([]byte, error) { + // 必须使用 JSON 字符串格式:`"文本内容"` + jsonText := fmt.Sprintf(`"%s"`, text) + + // 创建带超时的 HTTP 客户端(180秒超时,1.7B 模型需要更长时间) + client := &http.Client{ + Timeout: 180 * time.Second, + } + + resp, err := client.Post( + "http://localhost:8000/tts", + "application/json", + bytes.NewBufferString(jsonText), + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result TTSResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + if result.Code != 0 { + return nil, fmt.Errorf("TTS error: %s", result.Msg) + } + + return base64.StdEncoding.DecodeString(result.Audio) +} + +func main() { + // 长文本测试 + longText := "欢迎使用红动未来数字人服务平台,我们将为您提供最优质的AI数字人解决方案。人工智能技术正在改变我们的生活,让我们一起探索未来的无限可能。" + + audio, err := TTS(longText) + if err != nil { + panic(err) + } + + os.WriteFile("output.wav", audio, 0644) + fmt.Println("音频已保存到 output.wav") +} +``` + +### Python 调用示例(带超时设置) + +```python +import requests +import base64 + +def tts(text, timeout=180): + """ + TTS 文本转语音 + + Args: + text: 要转换的文本 + timeout: 超时时间(秒),1.7B 模型建议 >= 180 秒 + """ + # 必须使用 JSON 字符串格式 + response = requests.post( + "http://localhost:8000/tts", + data=f'"{text}"', + headers={"Content-Type": "application/json"}, + timeout=timeout # 设置超时 + ) + + result = response.json() + if result["code"] != 0: + raise Exception(f"TTS error: {result['msg']}") + + return base64.b64decode(result["audio"]) + +# 使用 +short_text = "你好,这是一个测试" +long_text = "欢迎使用红动未来数字人服务平台,我们将为您提供最优质的AI数字人解决方案。" + +# 短文本(30秒超时) +audio_data = tts(short_text, timeout=30) +with open("short_output.wav", "wb") as f: + f.write(audio_data) + +# 长文本(180秒超时) +audio_data = tts(long_text, timeout=180) +with open("long_output.wav", "wb") as f: + f.write(audio_data) +``` + +--- + +## 服务管理 + +```bash +# 查看日志 +docker logs -f tts-service + +# 查看最近50行日志 +docker logs --tail 50 tts-service + +# 停止服务 +docker stop tts-service + +# 启动服务 +docker start tts-service + +# 重启服务 +docker restart tts-service + +# 删除容器 +docker stop tts-service && docker rm tts-service + +# 删除镜像 +docker rmi qwen3-tts:latest + +# 进入容器 +docker exec -it tts-service /bin/bash +``` + +--- + +## 故障排查 + +### 1. 端口被占用 + +```bash +# 查找占用 8000 端口的进程 +lsof -ti:8000 + +# 停止占用端口的进程 +lsof -ti:8000 | xargs kill -9 + +# 或修改映射端口 +docker run -d -p 8001:8000 --name tts-service qwen3-tts:latest +``` + +### 2. API 返回 422 错误 + +**原因:** 请求格式不正确,必须使用 JSON 字符串格式 + +**正确请求:** +```bash +curl -X POST http://localhost:8000/tts \ + -H "Content-Type: application/json" \ + -d '"你好"' +``` + +**错误请求:** +```bash +# ❌ 错误:这是 JSON 对象,不是字符串 +curl -X POST http://localhost:8000/tts \ + -d '{"text": "你好"}' +``` + +### 3. 长文本返回 EOF 错误 + +**原因:** 1.7B 模型推理更慢,长文本处理时间超过客户端超时时间 + +**解决方案:** +1. **客户端设置超时 >= 180 秒** +2. **缩短文本长度**(建议单次请求 < 100 字) +3. **使用 GPU 加速**(如果可用) +4. **切换到 0.6B 模型**(更快但质量略低) + +**Go 客户端:** +```go +client := &http.Client{ + Timeout: 180 * time.Second, // 设置 180 秒超时 +} +``` + +**Python 客户端:** +```python +response = requests.post( + "http://localhost:8000/tts", + data=f'"{text}"', + timeout=180 # 设置 180 秒超时 +) +``` + +### 4. 音频无声音或文件过小 + +```bash +# 查看日志检查模型是否加载 +docker logs tts-service | grep "TTS 模型初始化完成" + +# 检查模型文件 +docker exec tts-service ls -la /app/model/ + +# 测试 API 返回的音频数据大小(应该 > 10KB) +curl -s http://localhost:8000/tts -d '"测试"' | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d['audio']))" +``` + +### 5. 内存不足 + +```bash +# 增加内存限制(1.7B 模型建议 >= 8GB) +docker run -d --name tts-service -p 8000:8000 --memory="8g" qwen3-tts:latest + +# 或增加更多内存 +docker run -d --name tts-service -p 8000:8000 --memory="12g" qwen3-tts:latest +``` + +### 6. 服务启动后无法访问 + +```bash +# 检查容器状态 +docker ps | grep tts-service + +# 检查端口映射 +docker port tts-service + +# 检查服务是否正常响应 +curl http://localhost:8000/ +``` + +### 7. 推理速度过慢 + +**1.7B 模型优化方案:** +```bash +# 限制并发为 1(避免 CPU 争抢) +docker run -d --name tts-service -p 8000:8000 qwen3-tts:latest \ + uvicorn app:app --limit-concurrency 1 + +# 增加 CPU 资源 +docker run -d --name tts-service -p 8000:8000 --cpus="8.0" qwen3-tts:latest +``` + +**GPU 加速(需要 NVIDIA GPU):** +```bash +# 修改 app.py 中的 device_map='cpu' 为 device_map='cuda:0' +# 重新构建镜像并运行 +docker run -d --name tts-service --gpus all -p 8000:8000 qwen3-tts:latest +``` + +**切换到更快的模型:** +- 如果对速度要求高,可切换到 `Qwen3-TTS-12Hz-0.6B-CustomVoice` +- 推理速度可提升 3-5 倍 + +### 8. 模型加载失败 + +**检查模型路径:** +```bash +# 查看容器内模型目录 +docker exec tts-service ls -la /app/model/ + +# 确认 app.py 中的 MODEL_PATH 和 TOKENIZER_PATH 正确 +docker exec tts-service cat /app/app.py | grep "MODEL_PATH\|TOKENIZER_PATH" +``` + +--- + +## 常见问题 FAQ + +**Q: 为什么选择 Qwen3-TTS-24Hz-1.7B-Base-VoiceClone?** + +A: 这是 Qwen3-TTS 系列中功能最全面、音质最高的模型: +- **24kHz 采样率**:双倍于 12Hz 模型,音质更清晰自然 +- **1.7B 参数**:模型表达能力最强 +- **声音克隆**:支持自定义声音训练和生成 + +**Q: 1.7B 模型推理速度慢怎么办?** + +A: 可以采取以下措施: +1. 客户端设置超时 >= 180 秒 +2. 使用 GPU 加速(速度提升 10-20 倍) +3. 切换到 0.6B 模型(速度提升 3-5 倍) +4. 缩短单次请求文本长度 + +**Q: 为什么 Body 必须是 JSON 字符串而不是 JSON 对象?** + +A: FastAPI 使用 `Body(..., media_type='application/json')` 解析时,直接接收 JSON 字符串。如果使用 `{"text": "..."}` 格式,需要修改 `app.py` 使用 Pydantic 模型。当前实现更简洁,直接传递字符串即可。 + +**Q: 24Hz 和 12Hz 模型有什么区别?** + +A: +- **24Hz**:采样率 24kHz,音质更清晰自然,适合高质量需求 +- **12Hz**:采样率 12kHz,推理速度快,适合实时应用 + +**Q: 1.7B 和 0.6B 模型有什么区别?** + +A: +- **1.7B**:参数量更大,音质更高,但推理速度慢,内存占用大(推荐 GPU) +- **0.6B**:参数量小,推理快,内存占用少,适合 CPU 环境 + +**Q: CustomVoice 和 Base 模型有什么区别?** + +A: +- **CustomVoice**:内置 9 种预设声音,开箱即用 +- **Base**:基础模型,支持自定义训练和声音克隆 + +**Q: 长文本推理需要多长时间?** + +A: 1.7B 模型 CPU 推理速度约 1-3 秒/字符: +- 短文本(< 20 字):约 15-30 秒 +- 中等文本(20-50 字):约 50-120 秒 +- 长文本(50-100 字):约 120-240 秒 + +**Q: 为什么长文本会返回 EOF 错误?** + +A: 1.7B 模型推理时间长,如果客户端超时时间设置过短会断开连接。解决方案: +1. 客户端设置超时 >= 180 秒 +2. 缩短单次请求文本长度 +3. 使用 GPU 加速 +4. 切换到 0.6B 模型 + +**Q: 支持哪些声音?** + +A: CustomVoice 模型支持 9 种预设声音:serena, vivian, uncle_fu, ryan, aiden, ono_anna, sohee, eric, dylan + +**Q: 可以自定义声音吗?** + +A: 可以,Base-VoiceClone 模型支持声音克隆功能,详见 Qwen3-TTS 官方文档 + +**Q: 支持哪些语言?** + +A: 中文、英文、日语、韩语、德语、法语、俄语、葡萄牙语、西班牙语、意大利语 + +**Q: 音频采样率是多少?** + +A: 24kHz (24000 Hz) - 比标准 CD 音质(44.1kHz)略低,但比 12Hz 模型清晰很多 + +**Q: 生成的音频文件格式是什么?** + +A: WAV 格式,Microsoft PCM, 16 bit, mono + +**Q: 如何提高推理速度?** + +A: +1. 使用 GPU 加速(device_map='cuda:0') +2. 使用更小的模型(0.6B 而非 1.7B) +3. 使用 12Hz 模型而非 24Hz 模型 +4. 限制并发请求(limit-concurrency=1) +5. 增加 CPU 核心数(--cpus="8.0") + +--- + +## 目录结构 + +``` +~/Qwen3-TTS/ +├── app.py # FastAPI 服务代码 +├── Dockerfile # Docker 镜像构建文件 +├── requirements.txt # Python 依赖 +└── model/ # 模型文件目录 + ├── Qwen3-TTS-Tokenizer-24Hz/ # 24Hz 分词器(651MB) + └── Qwen3-TTS-24Hz-1.7B-Base-VoiceClone/ # 24Hz 1.7B 模型(6.8GB) +``` + +--- + +## 相关链接 + +- 官方文档:https://github.com/QwenLM/Qwen3-TTS +- ModelScope:https://modelscope.cn/models?name=Qwen3-TTS +- FastAPI 文档:https://fastapi.tiangolo.com/ diff --git a/digitalhuman/go.mod b/digitalhuman/go.mod new file mode 100644 index 0000000..e7863c7 --- /dev/null +++ b/digitalhuman/go.mod @@ -0,0 +1,99 @@ +module digital-human + +go 1.26.0 + +require ( + gitea.com/red-future/common v0.0.12 + github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 + github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 + github.com/gogf/gf/v2 v2.10.0 +) + +// replace gitea.com/red-future/common => ../common + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/bwmarrin/snowflake v0.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-ego/gse v1.0.2 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect + github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/flatbuffers v1.12.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/consul/api v1.26.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.100 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/redis/go-redis/v9 v9.12.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tiger1103/gfast-token v1.0.10 // indirect + github.com/tinylib/msgp v1.6.1 // indirect + github.com/vcaesar/cedar v0.30.0 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.opencensus.io v0.23.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/digitalhuman/go.sum b/digitalhuman/go.sum new file mode 100644 index 0000000..84d6753 --- /dev/null +++ b/digitalhuman/go.sum @@ -0,0 +1,486 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +gitea.com/red-future/common v0.0.12 h1:whaCAiH33orl0P+oDpxzC4VoNluHKNYKGZ+FcUWw85Q= +gitea.com/red-future/common v0.0.12/go.mod h1:3a7cwZNvgpKw5FzE8x5MZImd7NBePGXRGFSMjt90158= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-ego/gse v1.0.2 h1:+27lYFPhQEhA9igtdOsJPRKYL/k3TwYsxBF5jr6KFv4= +github.com/go-ego/gse v1.0.2/go.mod h1:Fy35G+q7VV7Et1zIKO8o/sW1kkugV3znXap/lF/11zc= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 h1:N/F9CuDdUZLoM1nVRqrDE/33pDZuhVxpNY4wYdeIaBs= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0/go.mod h1:x6uoJGfZOtirIRQls8xUlYzC6f7T/eULPUa9er368X0= +github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ= +github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k= +github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec= +github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88= +github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= +github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/consul/api v1.26.1 h1:5oSXOO5fboPZeW5SN+TdGFP/BILDgBm19OrPZ/pICIM= +github.com/hashicorp/consul/api v1.26.1/go.mod h1:B4sQTeaSO16NtynqrAdwOlahJ7IUDZM9cj2420xYL8A= +github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= +github.com/hashicorp/consul/sdk v0.15.0/go.mod h1:r/OmRRPbHOe0yxNahLw7G9x5WG17E1BIECMtCjcPSNo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8= +github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= +github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s= +github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/vcaesar/cedar v0.30.0 h1:9fSDpM7FTjjUdPiBUUa0MWYMRGSEcqgFXvppZcZ4d7Y= +github.com/vcaesar/cedar v0.30.0/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik= +github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4= +github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/digitalhuman/main.go b/digitalhuman/main.go new file mode 100644 index 0000000..48af233 --- /dev/null +++ b/digitalhuman/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "digital-human/controller" + + _ "gitea.com/red-future/common/config" + "gitea.com/red-future/common/http" + "gitea.com/red-future/common/jaeger" + _ "gitea.com/red-future/common/ragflow" + _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" + _ "github.com/gogf/gf/contrib/nosql/redis/v2" +) + +func main() { + ctx := context.Background() + defer jaeger.ShutDown(ctx) + // 注册路由 + http.RouteRegister([]interface{}{ + controller.Audio, + controller.CustomVoice, + controller.DigitalHuman, + controller.Video, + controller.AsyncTask, + }) + // 保持应用运行 + select {} +} diff --git a/digitalhuman/model/dto/async_task_ref_dto.go b/digitalhuman/model/dto/async_task_ref_dto.go new file mode 100644 index 0000000..d2b05d8 --- /dev/null +++ b/digitalhuman/model/dto/async_task_ref_dto.go @@ -0,0 +1,25 @@ +package dto + +import "github.com/gogf/gf/v2/frame/g" + +// SyncAsyncTasksReq 定时任务/业务侧轮询使用:同步处理中间件任务状态并转移结果 +type SyncAsyncTasksReq struct { + g.Meta `path:"/syncAsyncTasks" method:"post" tags:"异步任务" summary:"同步异步任务" dc:"扫描本服务待处理任务(task_id),批量查询 model-asynch 状态,成功则转移OSS并更新业务表"` + Limit int `p:"limit" json:"limit" dc:"单次处理上限(默认200)"` +} + +type SyncAsyncTasksItem struct { + TaskID string `json:"taskId"` + State int `json:"state"` + TableName string `json:"tableName"` + BizID string `json:"bizId"` + OssFile string `json:"ossFile"` + ErrorMsg string `json:"errorMsg"` +} + +type SyncAsyncTasksRes struct { + Total int `json:"total" dc:"本次扫描到的任务数"` + Handled int `json:"handled" dc:"本次成功处理数(含更新状态/转移)"` + List []SyncAsyncTasksItem `json:"list" dc:"任务明细"` +} + diff --git a/digitalhuman/model/dto/audio_dto.go b/digitalhuman/model/dto/audio_dto.go new file mode 100644 index 0000000..a039fed --- /dev/null +++ b/digitalhuman/model/dto/audio_dto.go @@ -0,0 +1,151 @@ +package dto + +import ( + "digital-human/consts" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// CreateAudioReq 创建音频请求 +type CreateAudioReq struct { + g.Meta `path:"/createAudio" method:"post" tags:"音频管理" summary:"创建音频" dc:"创建新的音频"` + // 基础信息 + Name string `json:"name" v:"required" dc:"音频名称"` + Description string `json:"description" dc:"音频描述"` + ScriptText string `json:"scriptText" v:"required" dc:"话术文本"` + // 音色配置 + Voice string `json:"voice" dc:"音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan,默认serena"` + VoiceType string `json:"voiceType" dc:"音色类型:preset/custom(预设/自定义),默认preset"` + CustomVoice string `json:"customVoice" dc:"自定义音色ID(用于声音克隆),voiceType=custom时必填"` +} + +// CreateAudioRes 创建音频响应 +type CreateAudioRes struct { + Id int64 `json:"id" dc:"音频ID"` +} + +// ListAudioReq 获取音频列表请求 +type ListAudioReq struct { + g.Meta `path:"/listAudios" method:"get" tags:"音频管理" summary:"获取音频列表" dc:"分页查询音频列表,支持多条件筛选"` + *beans.Page + Status consts.AudioStatus `json:"status" dc:"状态:0生成中/1成功/2失败"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +// ListAudioRes 获取音频列表响应 +type ListAudioRes struct { + List []*AudioListItem `json:"list" dc:"音频列表"` + Total int64 `json:"total" dc:"总数"` +} + +// AudioListItem 音频列表项 +type AudioListItem struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ScriptText string `json:"scriptText"` + AudioURL string `json:"audioUrl"` + Status consts.AudioStatus `json:"status"` + ErrorMsg string `json:"errorMsg"` + Duration int `json:"duration"` + ExternalID string `json:"externalId"` + Voice string `json:"voice"` + VoiceType string `json:"voiceType"` + CustomVoice string `json:"customVoice"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// GetAudioReq 获取音频详情请求 +type GetAudioReq struct { + g.Meta `path:"/getAudio" method:"get" tags:"音频管理" summary:"获取音频详情" dc:"获取音频详情"` + ID int64 `json:"id" v:"required" dc:"音频ID"` +} + +// GetAudioRes 获取音频详情响应 +type GetAudioRes struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ScriptText string `json:"scriptText"` + AudioURL string `json:"audioUrl"` + Status consts.AudioStatus `json:"status"` + ErrorMsg string `json:"errorMsg"` + Duration int `json:"duration"` + ExternalID string `json:"externalId"` + Voice string `json:"voice"` + VoiceType string `json:"voiceType"` + CustomVoice string `json:"customVoice"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// UpdateAudioReq 更新音频请求 +type UpdateAudioReq struct { + g.Meta `path:"/updateAudio" method:"put" tags:"音频管理" summary:"更新音频" dc:"更新音频信息"` + ID int64 `json:"id" v:"required" dc:"音频ID"` + // 基础信息 + Name string `json:"name" dc:"音频名称"` + Description string `json:"description" dc:"音频描述"` + // 音色配置 + Voice string `json:"voice" dc:"音色"` + VoiceType string `json:"voiceType" dc:"音色类型"` + CustomVoice string `json:"customVoice" dc:"自定义音色ID"` +} + +// DeleteAudioReq 删除音频请求 +type DeleteAudioReq struct { + g.Meta `path:"/deleteAudio" method:"delete" tags:"音频管理" summary:"删除音频" dc:"删除音频"` + ID int64 `json:"id" v:"required" dc:"音频ID"` +} + +// GenerateAudioReq 生成音频请求 +type GenerateAudioReq struct { + g.Meta `path:"/generateAudio" method:"post" tags:"音频管理" summary:"生成音频" dc:"根据话术文本生成音频"` + ID int64 `json:"id" v:"required" dc:"音频ID"` +} + +// GenerateAudioRes 生成音频响应 +type GenerateAudioRes struct { + TaskID string `json:"taskId" dc:"任务ID"` +} + +// TTSReq 文本转语音请求 +type TTSReq struct { + g.Meta `path:"/tts" method:"post" tags:"音频管理" summary:"文本转语音" dc:"将文本转换为语音,直接返回MP3二进制数据"` + Text string `json:"text" v:"required" dc:"要转换的文本内容"` + Voice string `json:"voice" dc:"音色:默认default"` + Speed int `json:"speed" dc:"语速:0.5-2.0,默认1.0"` +} + +// TTSRes 文本转语音响应(返回二进制MP3数据) +type TTSRes struct { + g.Meta `mime:"audio/mpeg"` + Data []byte `json:"-" dc:"MP3音频二进制数据"` +} + +// GetAudioStatusOptionsReq 获取音频状态选项请求 +type GetAudioStatusOptionsReq struct { + g.Meta `path:"/getAudioStatusOptions" method:"get" tags:"音频管理" summary:"获取音频状态选项" dc:"获取所有音频状态的选项列表"` +} + +// GetAudioStatusOptionsRes 获取音频状态选项响应 +type GetAudioStatusOptionsRes struct { + Options []consts.AudioStatusKeyValue `json:"options" dc:"音频状态选项列表"` +} + +// Qwen3TTSRequest Qwen3-TTS 请求结构 +type Qwen3TTSRequest struct { + Text string `json:"text"` + Speaker string `json:"speaker"` // 预设音色名或克隆音色ID + VoiceID string `json:"voice_id,omitempty"` // 克隆音色ID(可选) +} + +// Qwen3TTSResponse Qwen3-TTS 响应结构 +type Qwen3TTSResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Audio string `json:"audio"` // base64编码的音频数据 +} diff --git a/digitalhuman/model/dto/custom_voice_dto.go b/digitalhuman/model/dto/custom_voice_dto.go new file mode 100644 index 0000000..b8c6cd0 --- /dev/null +++ b/digitalhuman/model/dto/custom_voice_dto.go @@ -0,0 +1,67 @@ +package dto + +import ( + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// CreateCustomVoiceReq 创建自定义音色请求 +type CreateCustomVoiceReq struct { + g.Meta `path:"/createCustomVoice" method:"post" tags:"自定义音色" summary:"创建自定义音色" dc:"上传参考音频创建自定义音色"` + VoiceType string `json:"voiceType" v:"required" dc:"音色类型 design/clone(设计/克隆)"` + Name string `json:"name" v:"required" dc:"音色名称"` + Description string `json:"description" dc:"音色描述"` + Text string `json:"text" dc:"参考文本"` + // 参考音频 + ReferenceAudio []byte `json:"referenceAudio" dc:"参考音频数据(base64编码的WAV文件,voiceType=clone 时必填)"` +} + +// CreateCustomVoiceRes 创建自定义音色响应 +type CreateCustomVoiceRes struct { + VoiceID string `json:"voiceId" dc:"音色ID"` +} + +// ListCustomVoiceReq 获取自定义音色列表请求 +type ListCustomVoiceReq struct { + g.Meta `path:"/listCustomVoices" method:"get" tags:"自定义音色" summary:"获取自定义音色列表" dc:"分页查询自定义音色列表"` + *beans.Page +} + +// ListCustomVoiceRes 获取自定义音色列表响应 +type ListCustomVoiceRes struct { + List []*CustomVoiceItem `json:"list" dc:"自定义音色列表"` + Total int64 `json:"total" dc:"总数"` +} + +// CustomVoiceItem 自定义音色列表项 +type CustomVoiceItem struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Status int `json:"status" dc:"状态:0生成中/1成功/2失败"` + ErrorMsg string `json:"errorMsg" dc:"错误信息"` + OssFile string `json:"ossFile" dc:"结果文件OSS地址"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// DeleteCustomVoiceReq 删除自定义音色请求 +type DeleteCustomVoiceReq struct { + g.Meta `path:"/deleteCustomVoice" method:"delete" tags:"自定义音色" summary:"删除自定义音色" dc:"删除自定义音色"` + VoiceID string `json:"voiceId" v:"required" dc:"音色ID"` +} + +// Qwen3VoiceCloneRequest Qwen3-TTS 音色克隆请求 +type Qwen3VoiceCloneRequest struct { + Name string `json:"name"` // 音色名称 + Audio string `json:"audio"` // base64编码的参考音频 + Text string `json:"text"` // 参考文本(可选) +} + +// Qwen3VoiceCloneResponse Qwen3-TTS 音色克隆响应 +type Qwen3VoiceCloneResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + VoiceID string `json:"voice_id"` // 克隆后的音色ID +} diff --git a/digitalhuman/model/dto/digitalhuman_dto.go b/digitalhuman/model/dto/digitalhuman_dto.go new file mode 100644 index 0000000..1d590ba --- /dev/null +++ b/digitalhuman/model/dto/digitalhuman_dto.go @@ -0,0 +1,158 @@ +package dto + +import ( + "digital-human/consts" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// CreateDigitalHumanReq 创建数字人形象请求 +type CreateDigitalHumanReq struct { + g.Meta `path:"/createDigitalHuman" method:"post" tags:"数字人形象管理" summary:"创建数字人形象" dc:"创建新的数字人形象"` + // 基础信息 + Name string `json:"name" v:"required" dc:"数字人名称"` + Description string `json:"description" dc:"数字人描述"` + ImageURL string `json:"imageUrl" dc:"形象图片URL"` + VideoURL string `json:"videoUrl" dc:"形象视频URL"` + Status consts.DigitalHumanStatus `json:"status" dc:"状态:1启用/0停用" d:"1"` + Tags []string `json:"tags" dc:"标签"` + Gender consts.Gender `json:"gender" dc:"性别"` + Age consts.Age `json:"age" dc:"年龄段"` + Style consts.Style `json:"style" dc:"风格:商务/休闲/正式等"` + ExternalID string `json:"externalId" dc:"外部系统ID"` + Metadata []map[string]interface{} `json:"metadata" dc:"动态元数据"` +} + +// CreateDigitalHumanRes 创建数字人形象响应 +type CreateDigitalHumanRes struct { + Id int64 `json:"id" dc:"数字人形象ID"` +} + +// ListDigitalHumanReq 获取数字人形象列表请求 +type ListDigitalHumanReq struct { + g.Meta `path:"/listDigitalHumans" method:"get" tags:"数字人形象管理" summary:"获取数字人形象列表" dc:"分页查询数字人形象列表,支持多条件筛选"` + *beans.Page + Status consts.DigitalHumanStatus `json:"status" dc:"状态"` + Gender consts.Gender `json:"gender" dc:"性别"` + Style consts.Style `json:"style" dc:"风格"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +// ListDigitalHumanRes 获取数字人形象列表响应 +type ListDigitalHumanRes struct { + List []*DigitalHumanListItem `json:"list" dc:"数字人形象列表"` + Total int64 `json:"total" dc:"总数"` +} + +// DigitalHumanListItem 数字人形象列表项 +type DigitalHumanListItem struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ImageURL string `json:"imageUrl"` + VideoURL string `json:"videoUrl"` + Status consts.DigitalHumanStatus `json:"status"` + Tags []string `json:"tags"` + Gender consts.Gender `json:"gender"` + Age consts.Age `json:"age"` + Style consts.Style `json:"style"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// GetDigitalHumanReq 获取数字人形象详情请求 +type GetDigitalHumanReq struct { + g.Meta `path:"/getDigitalHuman" method:"get" tags:"数字人形象管理" summary:"获取数字人形象详情" dc:"获取数字人形象详情"` + ID int64 `json:"id" v:"required" dc:"数字人形象ID"` +} + +// GetDigitalHumanRes 获取数字人形象详情响应 +type GetDigitalHumanRes struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ImageURL string `json:"imageUrl"` + VideoURL string `json:"videoUrl"` + Status consts.DigitalHumanStatus `json:"status"` + Tags []string `json:"tags"` + Gender consts.Gender `json:"gender"` + Age consts.Age `json:"age"` + Style consts.Style `json:"style"` + ExternalID string `json:"externalId"` + Metadata []map[string]interface{} `json:"metadata"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// UpdateDigitalHumanReq 更新数字人形象请求 +type UpdateDigitalHumanReq struct { + g.Meta `path:"/updateDigitalHuman" method:"put" tags:"数字人形象管理" summary:"更新数字人形象" dc:"更新数字人形象信息"` + ID int64 `json:"id" v:"required" dc:"数字人形象ID"` + // 基础信息 + Name string `json:"name" dc:"数字人名称"` + Description string `json:"description" dc:"数字人描述"` + ImageURL string `json:"imageUrl" dc:"形象图片URL"` + VideoURL string `json:"videoUrl" dc:"形象视频URL"` + Status consts.DigitalHumanStatus `json:"status" dc:"状态:1启用/0停用"` + Tags []string `json:"tags" dc:"标签"` + Gender consts.Gender `json:"gender" dc:"性别"` + Age consts.Age `json:"age" dc:"年龄段"` + Style consts.Style `json:"style" dc:"风格:商务/休闲/正式等"` + ExternalID string `json:"externalId" dc:"外部系统ID"` + Metadata []map[string]interface{} `json:"metadata" dc:"动态元数据"` +} + +// UpdateDigitalHumanStatusReq 更新数字人形象状态请求 +type UpdateDigitalHumanStatusReq struct { + g.Meta `path:"/updateDigitalHumanStatus" method:"put" tags:"数字人形象管理" summary:"更新数字人形象状态" dc:"更新数字人形象状态"` + ID int64 `json:"id" v:"required" dc:"数字人形象ID"` + Status consts.DigitalHumanStatus `json:"status" v:"required|in:1,0" dc:"状态:1启用/0停用"` +} + +// DeleteDigitalHumanReq 删除数字人形象请求 +type DeleteDigitalHumanReq struct { + g.Meta `path:"/deleteDigitalHuman" method:"delete" tags:"数字人形象管理" summary:"删除数字人形象" dc:"删除数字人形象"` + ID int64 `json:"id" v:"required" dc:"数字人形象ID"` +} + +// GetDigitalHumanStatusOptionsReq 获取数字人状态选项请求 +type GetDigitalHumanStatusOptionsReq struct { + g.Meta `path:"/getDigitalHumanStatusOptions" method:"get" tags:"数字人形象管理" summary:"获取数字人状态选项" dc:"获取所有数字人状态的选项列表"` +} + +// GetDigitalHumanStatusOptionsRes 获取数字人状态选项响应 +type GetDigitalHumanStatusOptionsRes struct { + Options []consts.StatusKeyValue `json:"options" dc:"状态选项列表"` +} + +// GetGenderOptionsReq 获取性别选项请求 +type GetGenderOptionsReq struct { + g.Meta `path:"/getGenderOptions" method:"get" tags:"数字人形象管理" summary:"获取性别选项" dc:"获取所有性别选项列表"` +} + +// GetGenderOptionsRes 获取性别选项响应 +type GetGenderOptionsRes struct { + Options []consts.GenderKeyValue `json:"options" dc:"性别选项列表"` +} + +// GetAgeOptionsReq 获取年龄段选项请求 +type GetAgeOptionsReq struct { + g.Meta `path:"/getAgeOptions" method:"get" tags:"数字人形象管理" summary:"获取年龄段选项" dc:"获取所有年龄段选项列表"` +} + +// GetAgeOptionsRes 获取年龄段选项响应 +type GetAgeOptionsRes struct { + Options []consts.AgeKeyValue `json:"options" dc:"年龄段选项列表"` +} + +// GetStyleOptionsReq 获取风格选项请求 +type GetStyleOptionsReq struct { + g.Meta `path:"/getStyleOptions" method:"get" tags:"数字人形象管理" summary:"获取风格选项" dc:"获取所有风格选项列表"` +} + +// GetStyleOptionsRes 获取风格选项响应 +type GetStyleOptionsRes struct { + Options []consts.StyleKeyValue `json:"options" dc:"风格选项列表"` +} diff --git a/digitalhuman/model/dto/video_dto.go b/digitalhuman/model/dto/video_dto.go new file mode 100644 index 0000000..764b759 --- /dev/null +++ b/digitalhuman/model/dto/video_dto.go @@ -0,0 +1,131 @@ +package dto + +import ( + "digital-human/consts" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// CreateVideoReq 创建视频请求 +type CreateVideoReq struct { + g.Meta `path:"/createVideo" method:"post" tags:"视频管理" summary:"创建视频" dc:"创建新的视频任务"` + // 基础信息 + Name string `json:"name" v:"required" dc:"视频名称"` + Description string `json:"description" dc:"视频描述"` + DigitalHumanID int64 `json:"digitalHumanId" v:"required" dc:"数字人形象ID"` + AudioID int64 `json:"audioId" v:"required" dc:"音频ID"` + Resolution consts.Resolution `json:"resolution" dc:"分辨率:480p/720p/1080p/2k/4k/8k"` +} + +// CreateVideoRes 创建视频响应 +type CreateVideoRes struct { + Id int64 `json:"id" dc:"视频ID"` +} + +// ListVideoReq 获取视频列表请求 +type ListVideoReq struct { + g.Meta `path:"/listVideos" method:"get" tags:"视频管理" summary:"获取视频列表" dc:"分页查询视频列表,支持多条件筛选"` + *beans.Page + Status consts.VideoStatus `json:"status" dc:"状态:0生成中/1成功/2失败"` + DigitalHumanID int64 `json:"digitalHumanId" dc:"数字人形象ID"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +// ListVideoRes 获取视频列表响应 +type ListVideoRes struct { + List []*VideoListItem `json:"list" dc:"视频列表"` + Total int64 `json:"total" dc:"总数"` +} + +// VideoListItem 视频列表项 +type VideoListItem struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + DigitalHumanID int64 `json:"digitalHumanId"` + DigitalHumanName string `json:"digitalHumanName"` + AudioID int64 `json:"audioId"` + AudioURL string `json:"audioUrl"` + VideoURL string `json:"videoUrl"` + Status consts.VideoStatus `json:"status"` + ErrorMsg string `json:"errorMsg"` + Duration int `json:"duration"` + Resolution consts.Resolution `json:"resolution"` + ThumbnailURL string `json:"thumbnailUrl"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// GetVideoReq 获取视频详情请求 +type GetVideoReq struct { + g.Meta `path:"/getVideo" method:"get" tags:"视频管理" summary:"获取视频详情" dc:"获取视频详情"` + ID int64 `json:"id" v:"required" dc:"视频ID"` +} + +// GetVideoRes 获取视频详情响应 +type GetVideoRes struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + DigitalHumanID int64 `json:"digitalHumanId"` + DigitalHumanName string `json:"digitalHumanName"` + AudioID int64 `json:"audioId"` + AudioURL string `json:"audioUrl"` + VideoURL string `json:"videoUrl"` + Status consts.VideoStatus `json:"status"` + ErrorMsg string `json:"errorMsg"` + Duration int `json:"duration"` + Resolution consts.Resolution `json:"resolution"` + ThumbnailURL string `json:"thumbnailUrl"` + ExternalTaskID string `json:"externalTaskId"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +// UpdateVideoReq 更新视频请求 +type UpdateVideoReq struct { + g.Meta `path:"/updateVideo" method:"put" tags:"视频管理" summary:"更新视频" dc:"更新视频信息"` + ID int64 `json:"id" v:"required" dc:"视频ID"` + // 基础信息 + Name string `json:"name" dc:"视频名称"` + Description string `json:"description" dc:"视频描述"` +} + +// DeleteVideoReq 删除视频请求 +type DeleteVideoReq struct { + g.Meta `path:"/deleteVideo" method:"delete" tags:"视频管理" summary:"删除视频" dc:"删除视频"` + ID int64 `json:"id" v:"required" dc:"视频ID"` +} + +// GenerateVideoReq 生成视频请求 +type GenerateVideoReq struct { + g.Meta `path:"/generateVideo" method:"post" tags:"视频管理" summary:"生成视频" dc:"选择数字人,选择已生成的音频,调用数字人形象与音频合成形成视频"` + ID int64 `json:"id" v:"required" dc:"视频ID"` +} + +// GenerateVideoRes 生成视频响应 +type GenerateVideoRes struct { + TaskID string `json:"taskId" dc:"任务ID"` +} + +// GetVideoStatusOptionsReq 获取视频状态选项请求 +type GetVideoStatusOptionsReq struct { + g.Meta `path:"/getVideoStatusOptions" method:"get" tags:"视频管理" summary:"获取视频状态选项" dc:"获取所有视频状态的选项列表"` +} + +// GetVideoStatusOptionsRes 获取视频状态选项响应 +type GetVideoStatusOptionsRes struct { + Options []consts.VideoStatusKeyValue `json:"options" dc:"视频状态选项列表"` +} + +// GetResolutionOptionsReq 获取分辨率选项请求 +type GetResolutionOptionsReq struct { + g.Meta `path:"/getResolutionOptions" method:"get" tags:"视频管理" summary:"获取分辨率选项" dc:"获取所有分辨率的选项列表"` +} + +// GetResolutionOptionsRes 获取分辨率选项响应 +type GetResolutionOptionsRes struct { + Options []consts.ResolutionKeyValue `json:"options" dc:"分辨率选项列表"` +} diff --git a/digitalhuman/model/entity/async_task_ref.go b/digitalhuman/model/entity/async_task_ref.go new file mode 100644 index 0000000..a3f90bf --- /dev/null +++ b/digitalhuman/model/entity/async_task_ref.go @@ -0,0 +1,38 @@ +package entity + +import ( + "gitea.com/red-future/common/beans" +) + +type asyncTaskRefCol struct { + beans.SQLBaseCol + TaskID string + State string + TableName string + BizID string + OssFile string + ErrorMsg string +} + +var AsyncTaskRefCol = asyncTaskRefCol{ + SQLBaseCol: beans.DefSQLBaseCol, + TaskID: "task_id", + State: "state", + TableName: "table_name", + BizID: "biz_id", + OssFile: "oss_file", + ErrorMsg: "error_msg", +} + +// AsyncTaskRef 异步任务绑定记录(中间件 task_id 绑定业务表) +// - state: 保存中间件返回的任务状态(0排队中/1执行中/2成功/3失败/4已下载) +type AsyncTaskRef struct { + beans.SQLBaseDO `orm:",inline"` + TaskID string `orm:"task_id" json:"taskId"` + State int `orm:"state" json:"state"` + TableName string `orm:"table_name" json:"tableName"` + BizID int64 `orm:"biz_id" json:"bizId,string"` + OssFile string `orm:"oss_file" json:"ossFile"` + ErrorMsg string `orm:"error_msg" json:"errorMsg"` +} + diff --git a/digitalhuman/model/entity/audio.go b/digitalhuman/model/entity/audio.go new file mode 100644 index 0000000..97cb7fe --- /dev/null +++ b/digitalhuman/model/entity/audio.go @@ -0,0 +1,55 @@ +package entity + +import ( + "digital-human/consts" + + "gitea.com/red-future/common/beans" +) + +type audioCol struct { + beans.SQLBaseCol + Name string + Description string + ScriptText string + AudioURL string + Status string + ErrorMsg string + Duration string + ExternalID string + Voice string + VoiceType string + CustomVoice string +} + +var AudioCol = audioCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Description: "description", + ScriptText: "script_text", + AudioURL: "audio_url", + Status: "status", + ErrorMsg: "error_msg", + Duration: "duration", + ExternalID: "external_id", + Voice: "voice", + VoiceType: "voice_type", + CustomVoice: "custom_voice", +} + +// Audio 音频实体 +type Audio struct { + beans.SQLBaseDO `orm:",inline"` + // 基础信息 + Name string `orm:"name" json:"name"` // 音频名称 + Description string `orm:"description" json:"description"` // 音频描述 + ScriptText string `orm:"script_text" json:"scriptText"` // 话术文本 + AudioURL string `orm:"audio_url" json:"audioUrl"` // 音频文件URL + Status consts.AudioStatus `orm:"status" json:"status"` // 状态:0生成中/1成功/2失败 + ErrorMsg string `orm:"error_msg" json:"errorMsg"` // 错误信息 + Duration int `orm:"duration" json:"duration"` // 音频时长(秒) + ExternalID string `orm:"external_id" json:"externalId"` // 外部音频ID + // 音色相关 + Voice string `orm:"voice" json:"voice"` // 音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan + VoiceType string `orm:"voice_type" json:"voiceType"` // 音色类型:preset/custom(预设/克隆) + CustomVoice string `orm:"custom_voice" json:"customVoice"` // 自定义音色ID(用于声音克隆) +} diff --git a/digitalhuman/model/entity/custom_voice.go b/digitalhuman/model/entity/custom_voice.go new file mode 100644 index 0000000..ecca640 --- /dev/null +++ b/digitalhuman/model/entity/custom_voice.go @@ -0,0 +1,38 @@ +package entity + +import ( + "gitea.com/red-future/common/beans" +) + +type customVoiceCol struct { + beans.SQLBaseCol + Name string + Description string + Status string + ErrorMsg string + OssFile string + ReferenceAudio string +} + +var CustomVoiceCol = customVoiceCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Description: "description", + Status: "status", + ErrorMsg: "error_msg", + OssFile: "oss_file", + ReferenceAudio: "reference_audio", +} + +// CustomVoice 自定义音色实体 +type CustomVoice struct { + beans.SQLBaseDO `orm:",inline"` + // 基础信息 + Name string `orm:"name" json:"name"` // 音色名称 + Description string `orm:"description" json:"description"` // 音色描述 + Text string `orm:"text" json:"text"` // 参考文本 + Status int `orm:"status" json:"status"` // 状态:0生成中/1成功/2失败 + ErrorMsg string `orm:"error_msg" json:"errorMsg"` // 错误信息 + OssFile string `orm:"oss_file" json:"ossFile"` // 结果文件URL(如参考音频/特征文件等) + ReferenceAudio []byte `orm:"reference_audio" json:"referenceAudio"` // 参考音频数据(二进制) +} diff --git a/digitalhuman/model/entity/digitalhuman.go b/digitalhuman/model/entity/digitalhuman.go new file mode 100644 index 0000000..6d50d12 --- /dev/null +++ b/digitalhuman/model/entity/digitalhuman.go @@ -0,0 +1,57 @@ +package entity + +import ( + "digital-human/consts" + + "gitea.com/red-future/common/beans" +) + +type digitalHumanCol struct { + beans.SQLBaseCol + Name string + Description string + AvatarURL string + VideoURL string + Voice string + Status string + Tags string + Gender string + Age string + Style string + ExternalID string + Metadata string +} + +var DigitalHumanCol = digitalHumanCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Description: "description", + AvatarURL: "avatar_url", + VideoURL: "video_url", + Voice: "voice", + Status: "status", + Tags: "tags", + Gender: "gender", + Age: "age", + Style: "style", + ExternalID: "external_id", + Metadata: "metadata", +} + +// DigitalHuman 数字人形象实体 +type DigitalHuman struct { + beans.SQLBaseDO `orm:",inline"` + // 基础信息 + Name string `orm:"name" json:"name"` // 数字人名称 + Description string `orm:"description" json:"description"` // 数字人描述 + AvatarURL string `orm:"avatar_url" json:"imageUrl"` // 形象图片URL + VideoURL string `orm:"video_url" json:"videoUrl"` // 形象视频URL + Voice string `orm:"voice" json:"voice"` // 默认音色 + Status consts.DigitalHumanStatus `orm:"status" json:"status"` // 状态:1启用/0停用 + Tags []string `orm:"tags" json:"tags"` // 标签 + Gender consts.Gender `orm:"gender" json:"gender"` // 性别 + Age consts.Age `orm:"age" json:"age"` // 年龄段 + Style consts.Style `orm:"style" json:"style"` // 风格:商务/休闲/正式等 + ExternalID string `orm:"external_id" json:"externalId"` // 外部系统ID + Metadata []map[string]interface{} `orm:"metadata" json:"metadata"` // 动态元数据 +} diff --git a/digitalhuman/model/entity/video.go b/digitalhuman/model/entity/video.go new file mode 100644 index 0000000..0890bbc --- /dev/null +++ b/digitalhuman/model/entity/video.go @@ -0,0 +1,60 @@ +package entity + +import ( + "digital-human/consts" + + "gitea.com/red-future/common/beans" +) + +type videoCol struct { + beans.SQLBaseCol + Name string + Description string + AudioID string + ScriptText string + VideoURL string + Status string + ErrorMsg string + Duration string + ThumbnailURL string + ExternalID string + DigitalHumanID string + DigitalHumanName string + Resolution string +} + +var VideoCol = videoCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Description: "description", + AudioID: "audio_id", + ScriptText: "script_text", + VideoURL: "video_url", + Status: "status", + ErrorMsg: "error_msg", + Duration: "duration", + ThumbnailURL: "thumbnail_url", + ExternalID: "external_id", + DigitalHumanID: "digital_human_id", + DigitalHumanName: "digital_human_name", + Resolution: "resolution", +} + +// Video 视频实体 +type Video struct { + beans.SQLBaseDO `orm:",inline"` + // 基础信息 + Name string `orm:"name" json:"name"` // 视频名称 + Description string `orm:"description" json:"description"` // 视频描述 + DigitalHumanID int64 `orm:"digital_human_id" json:"digitalHumanId"` // 数字人形象ID + AudioID int64 `orm:"audio_id" json:"audioId"` // 音频ID + ScriptText string `orm:"script_text" json:"scriptText"` // 话术文本 + VideoURL string `orm:"video_url" json:"videoUrl"` // 合成视频URL + Status consts.VideoStatus `orm:"status" json:"status"` // 状态:0生成中/1成功/2失败 + ErrorMsg string `orm:"error_msg" json:"errorMsg"` // 错误信息 + Duration int `orm:"duration" json:"duration"` // 视频时长(秒) + ThumbnailURL string `orm:"thumbnail_url" json:"thumbnailUrl"` // 缩略图URL + ExternalID string `orm:"external_id" json:"externalId"` // 外部任务ID + DigitalHumanName string `orm:"digital_human_name" json:"digitalHumanName"` // 数字人名称(冗余字段) + Resolution consts.Resolution `orm:"resolution" json:"resolution"` // 分辨率 +} diff --git a/digitalhuman/service/async_task_service.go b/digitalhuman/service/async_task_service.go new file mode 100644 index 0000000..92825b8 --- /dev/null +++ b/digitalhuman/service/async_task_service.go @@ -0,0 +1,195 @@ +package service + +import ( + "context" + "fmt" + + "digital-human/consts" + "digital-human/consts/public" + "digital-human/dao" + "digital-human/model/dto" + "digital-human/model/entity" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type asyncTaskService struct{} + +// AsyncTask 异步任务同步服务(供定时任务/业务轮询调用) +var AsyncTask = new(asyncTaskService) + +// Sync +// 1) 扫描 digital_human_async_task_ref 中 state=0/1 的记录(业务“生成中”) +// 2) 组装 task_id 批量请求 model-asynch /task/get-task-batch +// 3) 中间件状态映射到业务状态(业务只维护三态:0生成中/1成功/2失败): +// - 中间件 0/1/3(能查到 task_id) -> 业务 0(生成中) +// - 中间件 2/4(成功/已下载) -> 业务 1(成功) +// - 中间件 查不到 task_id(返回列表缺失) -> 业务 2(失败) +// +// 4) 绑定表仅用于“待同步列表”,因此: +// - 对中间件 0/1/3 不额外写库(减少查询/更新开销) +// - 对成功(2/4)与缺失(task_id 查不到)才更新绑定表 +func (s *asyncTaskService) Sync(ctx context.Context, req *dto.SyncAsyncTasksReq) (res *dto.SyncAsyncTasksRes, err error) { + limit := 200 + if req != nil && req.Limit > 0 { + limit = req.Limit + } + refs, err := dao.AsyncTaskRef.ListPending(ctx, limit) + if err != nil { + return nil, err + } + taskIDs := make([]string, 0, len(refs)) + refMap := make(map[string]*entity.AsyncTaskRef, len(refs)) + for _, r := range refs { + if r == nil || r.TaskID == "" { + continue + } + taskIDs = append(taskIDs, r.TaskID) + refMap[r.TaskID] = r + } + + out := &dto.SyncAsyncTasksRes{ + Total: len(taskIDs), + List: make([]dto.SyncAsyncTasksItem, 0, len(taskIDs)), + } + if len(taskIDs) == 0 { + return out, nil + } + items, err := getModelAsynchTaskBatch(ctx, taskIDs) + if err != nil { + return nil, err + } + + seen := make(map[string]struct{}, len(items)) + handled := 0 + + for _, it := range items { + r := refMap[it.TaskID] + if r == nil { + continue + } + seen[it.TaskID] = struct{}{} + + switch it.State { + case 0, 1, 3: + // 排队中/执行中/失败(可能重试):业务侧仍视为生成中,不更新绑定表,减少更新开销 + case 2, 4: + // 成功/已下载:业务侧写入 oss_file 并标记成功 + if it.OssFile == "" { + errMsg := "中间件返回空oss地址" + _ = s.updateBizFailed(ctx, r, errMsg) + _, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, it.TaskID, gdb.Map{ + entity.AsyncTaskRefCol.State: it.State, + entity.AsyncTaskRefCol.OssFile: "", + entity.AsyncTaskRefCol.ErrorMsg: errMsg, + }) + out.List = append(out.List, dto.SyncAsyncTasksItem{ + TaskID: it.TaskID, + State: it.State, + TableName: r.TableName, + BizID: fmt.Sprintf("%d", r.BizID), + OssFile: "", + ErrorMsg: errMsg, + }) + continue + } + if err := s.updateBizSuccess(ctx, r, it.OssFile); err != nil { + errMsg := fmt.Sprintf("生成音频失败: %v", err) + _ = s.updateBizFailed(ctx, r, errMsg) + _, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, it.TaskID, gdb.Map{ + entity.AsyncTaskRefCol.State: it.State, + entity.AsyncTaskRefCol.OssFile: it.OssFile, + entity.AsyncTaskRefCol.ErrorMsg: errMsg, + }) + out.List = append(out.List, dto.SyncAsyncTasksItem{ + TaskID: it.TaskID, + State: it.State, + TableName: r.TableName, + BizID: fmt.Sprintf("%d", r.BizID), + OssFile: it.OssFile, + ErrorMsg: errMsg, + }) + continue + } + handled++ + _, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, it.TaskID, gdb.Map{ + entity.AsyncTaskRefCol.State: it.State, + entity.AsyncTaskRefCol.OssFile: it.OssFile, + entity.AsyncTaskRefCol.ErrorMsg: "", + }) + default: + // 其他状态:不处理 + } + + out.List = append(out.List, dto.SyncAsyncTasksItem{ + TaskID: it.TaskID, + State: it.State, + TableName: r.TableName, + BizID: fmt.Sprintf("%d", r.BizID), + OssFile: it.OssFile, + ErrorMsg: "", + }) + } + + // 处理“查不到 task_id”的情况: + // 中间件对失败重试耗尽的任务会硬删除,批量接口不会返回该 task_id。 + // 业务侧把这种情况视为失败终态,并软删除绑定记录,避免重复轮询。 + for _, taskID := range taskIDs { + if _, ok := seen[taskID]; ok { + continue + } + r := refMap[taskID] + if r == nil { + continue + } + msg := "模型任务不存在已失败" + _ = s.updateBizFailed(ctx, r, msg) + _, _ = dao.AsyncTaskRef.UpdateByTaskID(ctx, taskID, gdb.Map{ + entity.AsyncTaskRefCol.State: 3, + entity.AsyncTaskRefCol.ErrorMsg: msg, + "deleted_at": gtime.Now(), + }) + out.List = append(out.List, dto.SyncAsyncTasksItem{ + TaskID: taskID, + State: 3, + TableName: r.TableName, + BizID: fmt.Sprintf("%d", r.BizID), + OssFile: "", + ErrorMsg: msg, + }) + } + + out.Handled = handled + g.Log().Infof(ctx, "[AsyncTask.Sync] total=%d handled=%d", out.Total, out.Handled) + return out, nil +} + +// updateBizSuccess 更新业务侧状态为成功 +func (s *asyncTaskService) updateBizSuccess(ctx context.Context, ref *entity.AsyncTaskRef, ossFile string) error { + switch ref.TableName { + case public.TableNameAudio: + _, err := dao.Audio.UpdateStatus(ctx, ref.BizID, consts.AudioStatusSuccess, "", ossFile, 0, "") + return err + case public.TableNameCustomVoice: + _, err := dao.CustomVoice.UpdateStatus(ctx, ref.BizID, 1, "", ossFile) + return err + default: + return fmt.Errorf("未知 table_name=%s", ref.TableName) + } +} + +// updateBizFailed 更新业务侧状态为失败 +func (s *asyncTaskService) updateBizFailed(ctx context.Context, ref *entity.AsyncTaskRef, msg string) error { + switch ref.TableName { + case public.TableNameAudio: + _, err := dao.Audio.UpdateStatus(ctx, ref.BizID, consts.AudioStatusFailed, msg, "", 0, "") + return err + case public.TableNameCustomVoice: + _, err := dao.CustomVoice.UpdateStatus(ctx, ref.BizID, 2, msg, "") + return err + default: + return nil + } +} diff --git a/digitalhuman/service/audio_service.go b/digitalhuman/service/audio_service.go new file mode 100644 index 0000000..ea9eec7 --- /dev/null +++ b/digitalhuman/service/audio_service.go @@ -0,0 +1,268 @@ +package service + +import ( + "context" + "encoding/base64" + + "digital-human/consts" + "digital-human/consts/public" + "digital-human/dao" + "digital-human/model/dto" + "digital-human/model/entity" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +type audio struct{} + +// Audio 音频服务 +var Audio = new(audio) + +// UploadFileResponse OSS 文件上传响应结构 +type UploadFileResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + FileURL string `json:"fileURL" dc:"上传地址"` + FileSize int `json:"fileSize" dc:"文件大小"` + FileName string `json:"fileName" dc:"文件名称"` + FileFormat string `json:"fileFormat" dc:"文件格式"` + FileAddressPrefix string `json:"fileAddressPrefix"` + } `json:"data"` +} + +// Create 创建音频 +func (s *audio) Create(ctx context.Context, req *dto.CreateAudioReq) (res *dto.CreateAudioRes, err error) { + // 设置默认音色 + if req.Voice == "" { + req.Voice = "Serena" // 默认音色 + } + if req.VoiceType == "" { + req.VoiceType = "Preset" // 默认预设音色 + } + + // 如果是自定义音色,验证音色是否存在 + if req.VoiceType == "custom" && req.CustomVoice != "" { + customVoiceID := gconv.Int64(req.CustomVoice) + _, err := dao.CustomVoice.GetOne(ctx, customVoiceID) + if err != nil { + return nil, gerror.Wrapf(err, "自定义音色不存在: %s", req.CustomVoice) + } + } + + // 插入数据库(初始状态为生成中) + audioID, err := dao.Audio.Insert(ctx, req) + if err != nil { + return nil, err + } + + // 通过 model-asynch 创建异步任务(由中间件执行模型调用与产物落 OSS) + // 约定: + // - custom(克隆音色) -> base 模型(需要参考音频/参考文本) 否则 -> customvoice 模型 + var taskID string + if req.VoiceType == "custom" { + customVoiceID := gconv.Int64(req.CustomVoice) + // 1. 先获取自定义音色详情 + cv, err := dao.CustomVoice.GetOne(ctx, customVoiceID) + if err != nil { + _, _ = dao.Audio.UpdateStatus(ctx, audioID, consts.AudioStatusFailed, "获取自定义音色失败: "+err.Error(), "", 0, "") + return nil, err + } + // 2. 调用模型生成音频 + refAudioBase64 := base64.StdEncoding.EncodeToString(cv.ReferenceAudio) + xVectorOnlyMode := false + if cv.Text == "" { + xVectorOnlyMode = true + } + taskID, err = TTS.CreateBaseTask(asyncCtx(ctx), req.ScriptText, "Auto", cv.Text, cv.OssFile, refAudioBase64, xVectorOnlyMode, 1.0) + } else { + // 1. 调用模型生成音频 + taskID, err = TTS.CreateCustomVoiceTask(asyncCtx(ctx), req.ScriptText, req.Voice, "Auto", "", 1.0) + } + if err != nil { + _, _ = dao.Audio.UpdateStatus(ctx, audioID, consts.AudioStatusFailed, "创建异步任务失败: "+err.Error(), "", 0, "") + return nil, err + } + _, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{ + TaskID: taskID, + State: 0, + TableName: public.TableNameAudio, + BizID: audioID, + }) + res = &dto.CreateAudioRes{ + Id: audioID, + } + return +} + +// List 获取音频列表 +func (s *audio) List(ctx context.Context, req *dto.ListAudioReq) (res *dto.ListAudioRes, err error) { + audioList, total, err := dao.Audio.List(ctx, req) + if err != nil { + return nil, err + } + res = &dto.ListAudioRes{ + Total: int64(total), + List: make([]*dto.AudioListItem, 0, len(audioList)), + } + for _, audio := range audioList { + res.List = append(res.List, &dto.AudioListItem{ + ID: audio.Id, + Name: audio.Name, + Description: audio.Description, + ScriptText: audio.ScriptText, + AudioURL: audio.AudioURL, + Status: audio.Status, + ErrorMsg: audio.ErrorMsg, + Duration: audio.Duration, + ExternalID: audio.ExternalID, + Voice: audio.Voice, + VoiceType: audio.VoiceType, + CustomVoice: audio.CustomVoice, + CreatedAt: audio.CreatedAt, + UpdatedAt: audio.UpdatedAt, + }) + } + return res, nil +} + +// GetOne 获取单个音频 +func (s *audio) GetOne(ctx context.Context, id int64) (*dto.GetAudioRes, error) { + audioOne, err := dao.Audio.GetOne(ctx, id) + if err != nil { + return nil, err + } + return &dto.GetAudioRes{ + ID: audioOne.Id, + Name: audioOne.Name, + Description: audioOne.Description, + ScriptText: audioOne.ScriptText, + AudioURL: audioOne.AudioURL, + Status: audioOne.Status, + ErrorMsg: audioOne.ErrorMsg, + Duration: audioOne.Duration, + ExternalID: audioOne.ExternalID, + Voice: audioOne.Voice, + VoiceType: audioOne.VoiceType, + CustomVoice: audioOne.CustomVoice, + CreatedAt: audioOne.CreatedAt, + UpdatedAt: audioOne.UpdatedAt, + }, nil +} + +// Update 更新音频 +func (s *audio) Update(ctx context.Context, req *dto.UpdateAudioReq) (err error) { + // 先获取原始音频信息 + audioOne, err := dao.Audio.GetOne(ctx, req.ID) + if err != nil { + return gerror.Wrap(err, "获取原始音频信息失败") + } + // 修改字段 + if !g.IsEmpty(req.Name) { + audioOne.Name = req.Name + } + if !g.IsEmpty(req.Description) { + audioOne.Description = req.Description + } + if !g.IsEmpty(req.Voice) { + audioOne.Voice = req.Voice + } + if !g.IsEmpty(req.VoiceType) { + audioOne.VoiceType = req.VoiceType + } + if !g.IsEmpty(req.CustomVoice) { + audioOne.CustomVoice = req.CustomVoice + } + _, err = dao.Audio.Update(ctx, req.ID, audioOne) + return err +} + +// Delete 删除音频 +func (s *audio) Delete(ctx context.Context, id int64) error { + _, err := dao.Audio.Delete(ctx, id) + return err +} + +// Generate 重新生成音频 +func (s *audio) Generate(ctx context.Context, req *dto.GenerateAudioReq) (res *dto.GenerateAudioRes, err error) { + // 获取音频信息 + audioOne, err := dao.Audio.GetOne(ctx, req.ID) + if err != nil { + return nil, gerror.Wrap(err, "获取音频信息失败") + } + + // 重置状态为生成中 + _, err = dao.Audio.UpdateStatus(ctx, req.ID, consts.AudioStatusGenerating, "", "", 0, "") + if err != nil { + return nil, err + } + + // 构建请求 + createReq := &dto.CreateAudioReq{ + Name: audioOne.Name, + Description: audioOne.Description, + ScriptText: audioOne.ScriptText, + Voice: audioOne.Voice, + VoiceType: audioOne.VoiceType, + CustomVoice: audioOne.CustomVoice, + } + + // 异步重新生成音频 + var taskID string + if createReq.VoiceType == "custom" { + customVoiceID := gconv.Int64(createReq.CustomVoice) + cv, err := dao.CustomVoice.GetOne(ctx, customVoiceID) + if err != nil { + _, _ = dao.Audio.UpdateStatus(ctx, req.ID, consts.AudioStatusFailed, "获取自定义音色失败: "+err.Error(), "", 0, "") + return nil, err + } + refAudioBase64 := "" + if cv != nil && len(cv.ReferenceAudio) > 0 { + refAudioBase64 = base64.StdEncoding.EncodeToString(cv.ReferenceAudio) + } + refText := "" + if cv != nil { + refText = cv.Text + } + xVectorOnlyMode := false + if refText == "" { + xVectorOnlyMode = true + } + taskID, err = TTS.CreateBaseTask(asyncCtx(ctx), createReq.ScriptText, "Auto", refText, cv.OssFile, refAudioBase64, xVectorOnlyMode, 1.0) + } else { + taskID, err = TTS.CreateCustomVoiceTask(asyncCtx(ctx), createReq.ScriptText, createReq.Voice, "Auto", "", 1.0) + } + if err != nil { + _, _ = dao.Audio.UpdateStatus(ctx, req.ID, consts.AudioStatusFailed, "创建异步任务失败: "+err.Error(), "", 0, "") + return nil, err + } + _, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{ + TaskID: taskID, + State: 0, + TableName: public.TableNameAudio, + BizID: req.ID, + }) + + res = &dto.GenerateAudioRes{ + TaskID: gconv.String(req.ID), + } + return +} + +// GetStatusOptions 获取状态选项 +func (s *audio) GetStatusOptions(ctx context.Context, req *dto.GetAudioStatusOptionsReq) (res *dto.GetAudioStatusOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetAudioStatusOptionsRes) + res.Options = consts.GetAllAudioStatusKeyValue() + return res, nil +} + +// TTS 文本转语音(使用 Qwen3-TTS) +func (s *audio) TTS(ctx context.Context, req *dto.TTSReq) (res *dto.TTSRes, err error) { + _ = ctx + _ = req + return nil, gerror.New("该接口已迁移为异步:请使用 CreateAudio 创建异步任务并通过轮询/批量领取获取结果") +} diff --git a/digitalhuman/service/custom_voice_service.go b/digitalhuman/service/custom_voice_service.go new file mode 100644 index 0000000..c528b63 --- /dev/null +++ b/digitalhuman/service/custom_voice_service.go @@ -0,0 +1,106 @@ +package service + +import ( + "context" + "encoding/base64" + + "digital-human/consts/public" + "digital-human/dao" + "digital-human/model/dto" + "digital-human/model/entity" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +type customVoice struct{} + +// CustomVoice 自定义音色服务 +var CustomVoice = new(customVoice) + +// CreateCustomVoice 创建自定义音色 +func (s *customVoice) CreateCustomVoice(ctx context.Context, req *dto.CreateCustomVoiceReq) (res *dto.CreateCustomVoiceRes, err error) { + g.Log().Infof(ctx, "创建自定义音色: name=%s, voiceType=%s", req.Name, req.VoiceType) + // 插入数据库(状态:生成中) + voiceID, err := dao.CustomVoice.Insert(ctx, req) + if err != nil { + return nil, err + } + + switch req.VoiceType { + case "design": + // 设计音频:按模型约定只传 text + instruct + taskID, err := TTS.CreateVoiceDesignTask(asyncCtx(ctx), req.Text, req.Description, "", 0) + if err != nil { + _, _ = dao.CustomVoice.UpdateStatus(ctx, voiceID, 2, "创建异步任务失败: "+err.Error(), "") + return nil, err + } + _, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{ + TaskID: taskID, + State: 0, + TableName: public.TableNameCustomVoice, + BizID: voiceID, + }) + res = &dto.CreateCustomVoiceRes{VoiceID: gconv.String(voiceID)} + g.Log().Infof(ctx, "自定义音色创建成功: voiceId=%d taskId=%s", voiceID, taskID) + case "clone": + // TODO : 克隆音色:使用语音转文字暂预留,后续找模型对应处理 + refAudioBase64 := base64.StdEncoding.EncodeToString(req.ReferenceAudio) + taskID, err := TTS.SpeechToText(asyncCtx(ctx), refAudioBase64) + if err != nil { + _, _ = dao.CustomVoice.UpdateStatus(ctx, voiceID, 2, "创建异步任务失败: "+err.Error(), "") + return nil, err + } + _, _ = dao.AsyncTaskRef.Insert(ctx, &entity.AsyncTaskRef{ + TaskID: taskID, + State: 0, + TableName: public.TableNameCustomVoice, + BizID: voiceID, + }) + res = &dto.CreateCustomVoiceRes{VoiceID: gconv.String(voiceID)} + g.Log().Infof(ctx, "克隆音色成功: voiceId=%d taskId=%s", voiceID, taskID) + default: + return nil, gerror.New("不支持的音色类型") + } + return +} + +// ListCustomVoices 获取自定义音色列表 +func (s *customVoice) ListCustomVoices(ctx context.Context, req *dto.ListCustomVoiceReq) (res *dto.ListCustomVoiceRes, err error) { + customVoices, total, err := dao.CustomVoice.List(ctx, req) + if err != nil { + return nil, err + } + + res = &dto.ListCustomVoiceRes{ + Total: int64(total), + List: make([]*dto.CustomVoiceItem, 0, len(customVoices)), + } + + for _, cv := range customVoices { + res.List = append(res.List, dao.CustomVoice.GetCustomVoiceItem(cv)) + } + + return +} + +// DeleteCustomVoice 删除自定义音色 +func (s *customVoice) DeleteCustomVoice(ctx context.Context, req *dto.DeleteCustomVoiceReq) (err error) { + // 验证音色是否存在 + voiceID := gconv.Int64(req.VoiceID) + + _, err = dao.CustomVoice.GetOne(ctx, voiceID) + if err != nil { + return gerror.Wrapf(err, "音色不存在: %s", req.VoiceID) + } + + // 删除音色 + _, err = dao.CustomVoice.Delete(ctx, voiceID) + if err != nil { + return gerror.Wrapf(err, "删除音色失败: %s", req.VoiceID) + } + + g.Log().Infof(ctx, "自定义音色删除成功: voiceId=%s", req.VoiceID) + return nil +} diff --git a/digitalhuman/service/digitalhuman_service.go b/digitalhuman/service/digitalhuman_service.go new file mode 100644 index 0000000..7670e8d --- /dev/null +++ b/digitalhuman/service/digitalhuman_service.go @@ -0,0 +1,179 @@ +package service + +import ( + "context" + "digital-human/consts" + "digital-human/dao" + "digital-human/model/dto" + "encoding/json" + "errors" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type digitalHuman struct{} + +// DigitalHuman 数字人形象服务 +var DigitalHuman = new(digitalHuman) + +// Create 创建数字人形象 +func (s *digitalHuman) Create(ctx context.Context, req *dto.CreateDigitalHumanReq) (res *dto.CreateDigitalHumanRes, err error) { + count, err := dao.DigitalHuman.Count(ctx, req) + if err != nil { + return nil, err + } + if count > 0 { + return nil, errors.New("数字人形象名称已存在") + } + // 插入数据库 + ids, err := dao.DigitalHuman.Insert(ctx, req) + if err != nil { + return nil, err + } + // PostgreSQL 使用 int64 + id := ids[0].(int64) + res = &dto.CreateDigitalHumanRes{ + Id: id, + } + return +} + +// List 获取数字人形象列表 +func (s *digitalHuman) List(ctx context.Context, req *dto.ListDigitalHumanReq) (res *dto.ListDigitalHumanRes, error error) { + digitalHumanList, total, err := dao.DigitalHuman.List(ctx, req) + if err != nil { + return + } + res = &dto.ListDigitalHumanRes{ + Total: total, + } + b, err := json.Marshal(digitalHumanList) + if err != nil { + return + } + err = json.Unmarshal(b, &res.List) + return +} + +// GetOne 获取单个数字人形象 +func (s *digitalHuman) GetOne(ctx context.Context, id int64) (*dto.GetDigitalHumanRes, error) { + digitalHumanOne, err := dao.DigitalHuman.GetOne(ctx, id) + if err != nil { + return nil, err + } + var createdAt, updatedAt *gtime.Time + if digitalHumanOne.CreatedAt != nil { + createdAt = digitalHumanOne.CreatedAt + } + if digitalHumanOne.UpdatedAt != nil { + updatedAt = digitalHumanOne.UpdatedAt + } + return &dto.GetDigitalHumanRes{ + ID: digitalHumanOne.Id, + Name: digitalHumanOne.Name, + Description: digitalHumanOne.Description, + ImageURL: digitalHumanOne.AvatarURL, + VideoURL: digitalHumanOne.VideoURL, + Status: digitalHumanOne.Status, + Tags: digitalHumanOne.Tags, + Gender: digitalHumanOne.Gender, + Age: digitalHumanOne.Age, + Style: digitalHumanOne.Style, + ExternalID: digitalHumanOne.ExternalID, + Metadata: digitalHumanOne.Metadata, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, nil +} + +// Update 更新数字人形象 +func (s *digitalHuman) Update(ctx context.Context, req *dto.UpdateDigitalHumanReq) error { + // 先获取原始数字人形象信息 + digitalHumanOne, err := dao.DigitalHuman.GetOne(ctx, req.ID) + if err != nil { + return gerror.Wrap(err, "获取原始数字人形象信息失败") + } + // 修改字段 + if !g.IsEmpty(req.Name) { + digitalHumanOne.Name = req.Name + } + if !g.IsEmpty(req.Description) { + digitalHumanOne.Description = req.Description + } + if !g.IsEmpty(req.ImageURL) { + digitalHumanOne.AvatarURL = req.ImageURL + } + if !g.IsEmpty(req.VideoURL) { + digitalHumanOne.VideoURL = req.VideoURL + } + digitalHumanOne.Status = req.Status + if req.Tags != nil { + digitalHumanOne.Tags = req.Tags + } + if !g.IsEmpty(req.Gender) { + digitalHumanOne.Gender = req.Gender + } + if !g.IsEmpty(req.Age) { + digitalHumanOne.Age = req.Age + } + if !g.IsEmpty(req.Style) { + digitalHumanOne.Style = req.Style + } + if !g.IsEmpty(req.ExternalID) { + digitalHumanOne.ExternalID = req.ExternalID + } + if req.Metadata != nil { + digitalHumanOne.Metadata = req.Metadata + } + + return dao.DigitalHuman.Update(ctx, req.ID, digitalHumanOne) +} + +// UpdateStatus 更新数字人形象状态 +func (s *digitalHuman) UpdateStatus(ctx context.Context, id int64, status consts.DigitalHumanStatus) error { + _, err := dao.DigitalHuman.UpdateStatus(ctx, id, status) + return err +} + +// Delete 删除数字人形象 +func (s *digitalHuman) Delete(ctx context.Context, id int64) error { + return dao.DigitalHuman.Delete(ctx, id) +} + +// GetStatusOptions 获取状态选项 +func (s *digitalHuman) GetStatusOptions(ctx context.Context, req *dto.GetDigitalHumanStatusOptionsReq) (res *dto.GetDigitalHumanStatusOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetDigitalHumanStatusOptionsRes) + res.Options = consts.GetAllStatusKeyValue() + return res, nil +} + +// GetGenderOptions 获取性别选项 +func (s *digitalHuman) GetGenderOptions(ctx context.Context, req *dto.GetGenderOptionsReq) (res *dto.GetGenderOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetGenderOptionsRes) + res.Options = consts.GetAllGenderKeyValue() + return res, nil +} + +// GetAgeOptions 获取年龄段选项 +func (s *digitalHuman) GetAgeOptions(ctx context.Context, req *dto.GetAgeOptionsReq) (res *dto.GetAgeOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetAgeOptionsRes) + res.Options = consts.GetAllAgeKeyValue() + return res, nil +} + +// GetStyleOptions 获取风格选项 +func (s *digitalHuman) GetStyleOptions(ctx context.Context, req *dto.GetStyleOptionsReq) (res *dto.GetStyleOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetStyleOptionsRes) + res.Options = consts.GetAllStyleKeyValue() + return res, nil +} diff --git a/digitalhuman/service/http_wrapper.go b/digitalhuman/service/http_wrapper.go new file mode 100644 index 0000000..d6e62e7 --- /dev/null +++ b/digitalhuman/service/http_wrapper.go @@ -0,0 +1,219 @@ +package service + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + stdhttp "net/http" + "os" + "path/filepath" + "sync" + "time" + + commonHttp "gitea.com/red-future/common/http" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" +) + +var commonHttpTransportMu sync.Mutex + +// asyncCtx 异步上下文处理 +func asyncCtx(ctx context.Context) context.Context { + asyncCtx := context.WithoutCancel(ctx) + if r := g.RequestFromCtx(ctx); r != nil { + if token := r.Header.Get("Authorization"); token != "" { + asyncCtx = context.WithValue(asyncCtx, "token", token) + } + } + if user, uErr := utils.GetUserInfo(ctx); uErr == nil && user != nil { + asyncCtx = context.WithValue(asyncCtx, "user", user) + } + return asyncCtx +} + +// setCommonHttpResponseHeaderTimeout 调整公共 HTTP 客户端响应头超时,避免长时推理被 30s 默认值打断。 +func setCommonHttpResponseHeaderTimeout(d time.Duration) { + if d <= 0 { + return + } + commonHttpTransportMu.Lock() + defer commonHttpTransportMu.Unlock() + if tr, ok := commonHttp.Httpclient.Transport.(*stdhttp.Transport); ok && tr != nil { + if tr.ResponseHeaderTimeout < d { + tr.ResponseHeaderTimeout = d + } + } +} + +// forwardHeaders 透传调用链路中必须的头信息,优先使用异步上下文里固化的 token。 +func forwardHeaders(ctx context.Context) map[string]string { + headers := make(map[string]string) + if token, ok := ctx.Value("token").(string); ok && token != "" { + headers["Authorization"] = token + } + if r := g.RequestFromCtx(ctx); r != nil { + if headers["Authorization"] == "" { + if token := r.Header.Get("Authorization"); token != "" { + headers["Authorization"] = token + } + } + if userInfo := r.Header.Get("X-User-Info"); userInfo != "" { + headers["X-User-Info"] = userInfo + } + } + return headers +} + +// commonPostJSON 使用 common/http 的底层客户端直连 JSON 接口,适配非统一响应包装结构。 +func commonPostJSON(ctx context.Context, url string, headers map[string]string, req any, resp any) error { + client := commonHttp.Httpclient.Clone().ContentJson() + if deadline, ok := ctx.Deadline(); ok { + if d := time.Until(deadline); d > 0 { + client.SetTimeout(d) + } + } + if len(headers) > 0 { + client.SetHeaderMap(headers) + } + r, err := client.DoRequest(ctx, stdhttp.MethodPost, url, req) + if err != nil { + return err + } + defer r.Close() + + body, err := io.ReadAll(r.Body) + if err != nil { + return gerror.Wrap(err, "读取响应失败") + } + if r.StatusCode != stdhttp.StatusOK { + return gerror.Newf("HTTP状态码异常: %d, body: %s", r.StatusCode, string(body)) + } + if err := json.Unmarshal(body, resp); err != nil { + return gerror.Wrapf(err, "解析响应失败, body: %s", string(body)) + } + return nil +} + +func commonPostMultipartFile(ctx context.Context, url string, headers map[string]string, form map[string]string, fileField string, filePath string, resp any) error { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + for k, v := range form { + if v == "" { + continue + } + if err := writer.WriteField(k, v); err != nil { + return gerror.Wrapf(err, "写入表单字段失败: %s", k) + } + } + + f, err := os.Open(filePath) + if err != nil { + return gerror.Wrapf(err, "打开文件失败: %s", filePath) + } + defer f.Close() + + part, err := writer.CreateFormFile(fileField, filepath.Base(filePath)) + if err != nil { + return gerror.Wrapf(err, "创建表单文件失败: %s", fileField) + } + if _, err := io.Copy(part, f); err != nil { + return gerror.Wrap(err, "写入文件内容失败") + } + + contentType := writer.FormDataContentType() + if err := writer.Close(); err != nil { + return gerror.Wrap(err, "关闭表单写入器失败") + } + + client := commonHttp.Httpclient.Clone() + if deadline, ok := ctx.Deadline(); ok { + if d := time.Until(deadline); d > 0 { + client.SetTimeout(d) + } + } + if headers == nil { + headers = make(map[string]string) + } + headers["Content-Type"] = contentType + client.SetHeaderMap(headers) + + r, err := client.DoRequest(ctx, stdhttp.MethodPost, url, body.Bytes()) + if err != nil { + return err + } + defer r.Close() + + raw, err := io.ReadAll(r.Body) + if err != nil { + return gerror.Wrap(err, "读取响应失败") + } + if r.StatusCode != stdhttp.StatusOK { + return gerror.Newf("HTTP状态码异常: %d, body: %s", r.StatusCode, string(raw)) + } + if err := json.Unmarshal(raw, resp); err != nil { + return gerror.Wrapf(err, "解析响应失败, body: %s", string(raw)) + } + return nil +} + +// -------------------------- model-asynch 调用封装 -------------------------- + +const modelAsynchServiceName = "model-asynch" + +type modelAsynchCreateTaskReq struct { + ModelName string `json:"modelName"` + InputRef string `json:"inputRef,omitempty"` + RequestPayload any `json:"requestPayload"` +} + +type modelAsynchCreateTaskRes struct { + TaskID string `json:"taskId"` +} + +// createModelAsynchTask 调用 model-asynch 创建任务 +// 注意:路由以 GoFrame 默认输出为准(通常为 /task/create-task) +func createModelAsynchTask(ctx context.Context, modelName string, payload any, inputRef string) (taskID string, err error) { + taskUrl := g.Cfg().MustGet(ctx, "model-asynch.addr", "127.0.0.1:8080") + headers := forwardHeaders(ctx) + req := &modelAsynchCreateTaskReq{ + ModelName: modelName, + InputRef: inputRef, + RequestPayload: payload, + } + var res modelAsynchCreateTaskRes + if err := commonHttp.Post(ctx, fmt.Sprintf("%s/task/createTask", taskUrl), headers, &res, req); err != nil { + return "", err + } + return res.TaskID, nil +} + +type modelAsynchBatchReq struct { + TaskIDs []string `json:"taskIds"` +} + +type modelAsynchBatchItem struct { + TaskID string `json:"taskId"` + State int `json:"state"` + OssFile string `json:"ossFile"` +} + +type modelAsynchBatchRes struct { + List []modelAsynchBatchItem `json:"list"` +} + +// getModelAsynchTaskBatch 批量查询任务(成功 2->4 的逻辑由中间件内部处理) +func getModelAsynchTaskBatch(ctx context.Context, taskIDs []string) (items []modelAsynchBatchItem, err error) { + taskUrl := g.Cfg().MustGet(ctx, "model-asynch.addr", "127.0.0.1:8080") + headers := forwardHeaders(ctx) + req := &modelAsynchBatchReq{TaskIDs: taskIDs} + var res modelAsynchBatchRes + if err := commonHttp.Post(ctx, fmt.Sprintf("%s/task/getTaskBatch", taskUrl), headers, &res, req); err != nil { + return nil, err + } + return res.List, nil +} diff --git a/digitalhuman/service/tts_service.go b/digitalhuman/service/tts_service.go new file mode 100644 index 0000000..2738467 --- /dev/null +++ b/digitalhuman/service/tts_service.go @@ -0,0 +1,117 @@ +package service + +import ( + "context" + "encoding/base64" + + "digital-human/consts/public" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" +) + +type tts struct{} + +// TTS 统一的模型异步调用封装(通过 model-asynch 中间件) +var TTS = new(tts) + +// CreateVoiceDesignTask 设计音频任务(VoiceDesign) +func (s *tts) CreateVoiceDesignTask( + ctx context.Context, + text string, + instruct string, + language string, // 空则 Auto + speed float64, // <=0 则 1.0 +) (taskID string, err error) { + if language == "" { + language = "Auto" + } + if speed <= 0 { + speed = 1.0 + } + payload := map[string]any{ + "text": text, + "language": language, + "instruct": instruct, + "speed": speed, + "response_format": "wav", + } + g.Log().Info(ctx, "[CreateVoiceDesignTask] %v", payload) + return createModelAsynchTask(ctx, public.ModelNameVoiceDesign, payload, "") +} + +// CreateCustomVoiceTask 预设音色(CustomVoice)任务 +// - speaker: 预设说话人(如 Vivian/Serena/Ryan/...) +// - instruct: 可选,情绪/风格控制 +func (s *tts) CreateCustomVoiceTask( + ctx context.Context, + text string, + speaker string, + language string, // 例如 "Chinese"/"English"/"Auto",空则默认 "Auto" + instruct string, // 可空 + speed float64, // 0.5~2.0,<=0 则默认 1.0 +) (taskID string, err error) { + if language == "" { + language = "Auto" + } + if speed <= 0 { + speed = 1.0 + } + payload := map[string]any{ + "text": text, + "language": language, + "speaker": speaker, + "instruct": instruct, + "speed": speed, + "response_format": "wav", // 建议统一用 wav + } + g.Log().Info(ctx, "[CreateCustomVoiceTask] %v", payload) + return createModelAsynchTask(ctx, public.ModelNameCustomVoice, payload, "") +} + +// CreateBaseTask 声音克隆(Base / clone)任务 +// 说明:ref_audio_url 与 ref_audio_base64 二选一 +func (s *tts) CreateBaseTask( + ctx context.Context, + text string, + language string, // 例如 "Chinese"/"English"/"Auto",空则默认 "Auto" + refText string, // 当 xVectorOnlyMode=false 时必填 + refAudioURL string, // 可空 + refAudioBase64 string, // 可空(不带 data: 前缀也可以) + xVectorOnlyMode bool, // true=不需要 refText,但质量可能下降 + speed float64, // 0.5~2.0,<=0 则默认 1.0 +) (taskID string, err error) { + if language == "" { + language = "Auto" + } + if speed <= 0 { + speed = 1.0 + } + + payload := map[string]any{ + "text": text, + "language": language, + "ref_text": refText, + "ref_audio_url": refAudioURL, + "ref_audio_base64": refAudioBase64, + "x_vector_only_mode": xVectorOnlyMode, + "speed": speed, + "response_format": "wav", + } + g.Log().Info(ctx, "[CreateBaseTask] %v", payload) + return createModelAsynchTask(ctx, public.ModelNameBase, payload, "") +} + +// SpeechToText 语音转文本(预留) +// audioBase64:base64 编码的音频数据(WAV/MP3等) +func (s *tts) SpeechToText(ctx context.Context, audioBase64 string) (text string, err error) { + _ = ctx + if audioBase64 == "" { + return "", gerror.New("audioBase64 不能为空") + } + // 简单校验 base64 合法性 + if _, err := base64.StdEncoding.DecodeString(audioBase64); err != nil { + return "", gerror.Wrap(err, "audioBase64 非法") + } + return "", gerror.New("SpeechToText 暂未实现:后续接入语音识别模型后补齐") +} diff --git a/digitalhuman/service/video_service.go b/digitalhuman/service/video_service.go new file mode 100644 index 0000000..3c64495 --- /dev/null +++ b/digitalhuman/service/video_service.go @@ -0,0 +1,218 @@ +package service + +import ( + "context" + "digital-human/consts" + "digital-human/dao" + "digital-human/model/dto" + "encoding/json" + "errors" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" +) + +type video struct{} + +// Video 视频服务 +var Video = new(video) + +// Create 创建视频 +func (s *video) Create(ctx context.Context, req *dto.CreateVideoReq) (res *dto.CreateVideoRes, err error) { + // 验证数字人形象是否存在且启用 + digitalHumanOne, err := dao.DigitalHuman.GetOne(ctx, req.DigitalHumanID) + if err != nil { + return nil, gerror.Wrap(err, "数字人形象不存在") + } + if digitalHumanOne.Status != consts.DigitalHumanStatusActive { + return nil, errors.New("数字人形象未启用") + } + + // 验证音频是否存在且已生成成功 + audioOne, err := dao.Audio.GetOne(ctx, req.AudioID) + if err != nil { + return nil, gerror.Wrap(err, "音频不存在") + } + if audioOne.Status != consts.AudioStatusSuccess { + return nil, errors.New("音频未生成成功,无法合成视频") + } + + // 创建视频记录(初始状态为生成中) + ids, err := dao.Video.Insert(ctx, req) + if err != nil { + return nil, err + } + + // 保存视频ID(PostgreSQL 使用 int64) + videoID := ids[0].(int64) + + // 异步生成视频 + go s.generateVideo(ctx, req.DigitalHumanID, digitalHumanOne.Name, req.AudioID, audioOne.AudioURL, audioOne.Duration, req.Resolution, videoID) + + res = &dto.CreateVideoRes{ + Id: videoID, + } + return +} + +// generateVideo 生成视频(异步处理) +func (s *video) generateVideo(ctx context.Context, digitalHumanID int64, digitalHumanName string, audioID int64, audioURL string, duration int, resolution consts.Resolution, videoID int64) { + // 更新视频状态,设置音频URL和时长 + _, _ = dao.Video.UpdateStatus(ctx, videoID, consts.VideoStatusGenerating, "", audioURL, duration, "", "") + + // 调用数字人形象与音频合成服务 + videoURL, thumbnailURL, externalTaskID, err := s.synthesizeVideo(ctx, digitalHumanID, audioURL, resolution) + if err != nil { + // 视频合成失败 + _, _ = dao.Video.UpdateStatus(ctx, videoID, consts.VideoStatusFailed, "视频合成失败: "+err.Error(), "", 0, "", "") + return + } + + // 更新视频生成状态为成功 + _, _ = dao.Video.UpdateStatus(ctx, videoID, consts.VideoStatusSuccess, "", videoURL, duration, thumbnailURL, externalTaskID) +} + +// synthesizeVideo 合成视频(模拟) +func (s *video) synthesizeVideo(ctx context.Context, digitalHumanID int64, audioURL string, resolution consts.Resolution) (videoURL string, thumbnailURL string, externalTaskID string, err error) { + // TODO: 调用真实的数字人视频合成服务API + // 这里模拟返回 + g.Log().Info(ctx, "合成视频,数字人ID:", digitalHumanID, "音频URL:", audioURL, "分辨率:", resolution) + + // 模拟外部任务ID(使用雪花算法或UUID) + externalTaskID = gconv.String(digitalHumanID) + "-" + gconv.String(gtime.Timestamp()) + + // 模拟视频URL(实际应该从视频合成服务获取) + videoURL = "https://example.com/video/" + externalTaskID + ".mp4" + + // 模拟缩略图URL + thumbnailURL = "https://example.com/video/" + externalTaskID + "_thumb.jpg" + + return videoURL, thumbnailURL, externalTaskID, nil +} + +// List 获取视频列表 +func (s *video) List(ctx context.Context, req *dto.ListVideoReq) (res *dto.ListVideoRes, error error) { + videoList, total, err := dao.Video.List(ctx, req) + if err != nil { + return + } + res = &dto.ListVideoRes{ + Total: total, + } + b, err := json.Marshal(videoList) + if err != nil { + return + } + err = json.Unmarshal(b, &res.List) + return +} + +// GetOne 获取单个视频 +func (s *video) GetOne(ctx context.Context, id int64) (*dto.GetVideoRes, error) { + videoOne, err := dao.Video.GetOne(ctx, id) + if err != nil { + return nil, err + } + var createdAt, updatedAt *gtime.Time + if videoOne.CreatedAt != nil { + createdAt = videoOne.CreatedAt + } + if videoOne.UpdatedAt != nil { + updatedAt = videoOne.UpdatedAt + } + return &dto.GetVideoRes{ + ID: videoOne.Id, + Name: videoOne.Name, + Description: videoOne.Description, + DigitalHumanID: videoOne.DigitalHumanID, + DigitalHumanName: videoOne.DigitalHumanName, + AudioID: videoOne.AudioID, + AudioURL: "", + VideoURL: videoOne.VideoURL, + Status: videoOne.Status, + ErrorMsg: videoOne.ErrorMsg, + Duration: videoOne.Duration, + Resolution: videoOne.Resolution, + ThumbnailURL: videoOne.ThumbnailURL, + ExternalTaskID: videoOne.ExternalID, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, nil +} + +// Update 更新视频 +func (s *video) Update(ctx context.Context, req *dto.UpdateVideoReq) error { + // 先获取原始视频信息 + videoOne, err := dao.Video.GetOne(ctx, req.ID) + if err != nil { + return gerror.Wrap(err, "获取原始视频信息失败") + } + // 修改字段 + if !g.IsEmpty(req.Name) { + videoOne.Name = req.Name + } + if !g.IsEmpty(req.Description) { + videoOne.Description = req.Description + } + + return dao.Video.Update(ctx, req.ID, videoOne) +} + +// Delete 删除视频 +func (s *video) Delete(ctx context.Context, id int64) error { + return dao.Video.Delete(ctx, id) +} + +// Generate 重新生成视频 +func (s *video) Generate(ctx context.Context, req *dto.GenerateVideoReq) (res *dto.GenerateVideoRes, err error) { + // 获取视频信息 + videoOne, err := dao.Video.GetOne(ctx, req.ID) + if err != nil { + return nil, gerror.Wrap(err, "获取视频信息失败") + } + + // 验证音频是否仍然有效(已生成成功) + if videoOne.AudioID != 0 { + audioOne, err := dao.Audio.GetOne(ctx, videoOne.AudioID) + if err != nil { + return nil, gerror.Wrap(err, "获取音频信息失败") + } + if audioOne.Status != consts.AudioStatusSuccess { + return nil, errors.New("音频未生成成功,无法合成视频") + } + } + + // 重置状态为生成中 + _, err = dao.Video.UpdateStatus(ctx, req.ID, consts.VideoStatusGenerating, "", "", 0, "", "") + if err != nil { + return nil, err + } + + // 异步重新生成视频 + go s.generateVideo(ctx, videoOne.DigitalHumanID, videoOne.DigitalHumanName, videoOne.AudioID, "", videoOne.Duration, videoOne.Resolution, req.ID) + + res = &dto.GenerateVideoRes{ + TaskID: gconv.String(req.ID), + } + return +} + +// GetStatusOptions 获取状态选项 +func (s *video) GetStatusOptions(ctx context.Context, req *dto.GetVideoStatusOptionsReq) (res *dto.GetVideoStatusOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetVideoStatusOptionsRes) + res.Options = consts.GetAllVideoStatusKeyValue() + return res, nil +} + +// GetResolutionOptions 获取分辨率选项 +func (s *video) GetResolutionOptions(ctx context.Context, req *dto.GetResolutionOptionsReq) (res *dto.GetResolutionOptionsRes, err error) { + _ = ctx + _ = req + res = new(dto.GetResolutionOptionsRes) + res.Options = consts.GetResolutionOptions() + return res, nil +} diff --git a/digitalhuman/update.sql b/digitalhuman/update.sql new file mode 100644 index 0000000..de7cb9e --- /dev/null +++ b/digitalhuman/update.sql @@ -0,0 +1,230 @@ +-- -----------------------张斌2025-06-16 15:00:00----------------------- + +--------------------pgsql创建digital_human_audio表语句--------------------------- +-- 音频表 +CREATE TABLE IF NOT EXISTS digital_human_audio ( + -- 基础字段(继承 SQLBaseCol 通用字段,与 SQLBaseDO 对齐) + id BIGINT PRIMARY KEY, -- 主键ID(非自增) + tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8类型 + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 音频核心字段 + name VARCHAR(128) NOT NULL, -- 音频名称 + description TEXT DEFAULT '', -- 音频描述 + script_text TEXT NOT NULL, -- 话术文本 + audio_url VARCHAR(512) DEFAULT '', -- 音频文件URL + status SMALLINT NOT NULL DEFAULT 0, -- 状态:0生成中/1成功/2失败 + error_msg TEXT DEFAULT '', -- 错误信息 + duration INT DEFAULT 0, -- 音频时长(秒) + external_id VARCHAR(64) DEFAULT '', -- 外部音频ID + voice VARCHAR(32) DEFAULT 'serena', -- 音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan + voice_type VARCHAR(16) DEFAULT 'preset', -- 音色类型:preset/custom(预设/克隆) + custom_voice VARCHAR(64) DEFAULT '' -- 自定义音色ID(用于声音克隆) +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_audio_tenant_id ON digital_human_audio(tenant_id); +CREATE INDEX IF NOT EXISTS idx_audio_status ON digital_human_audio(status); +CREATE INDEX IF NOT EXISTS idx_audio_voice_type ON digital_human_audio(voice_type); +CREATE INDEX IF NOT EXISTS idx_audio_deleted_at ON digital_human_audio(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE digital_human_audio IS '音频表'; +COMMENT ON COLUMN digital_human_audio.id IS '主键ID(非自增)'; +COMMENT ON COLUMN digital_human_audio.tenant_id IS '租户ID'; +COMMENT ON COLUMN digital_human_audio.creator IS '创建人'; +COMMENT ON COLUMN digital_human_audio.created_at IS '创建时间'; +COMMENT ON COLUMN digital_human_audio.updater IS '更新人'; +COMMENT ON COLUMN digital_human_audio.updated_at IS '更新时间'; +COMMENT ON COLUMN digital_human_audio.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN digital_human_audio.name IS '音频名称'; +COMMENT ON COLUMN digital_human_audio.description IS '音频描述'; +COMMENT ON COLUMN digital_human_audio.script_text IS '话术文本'; +COMMENT ON COLUMN digital_human_audio.audio_url IS '音频文件URL'; +COMMENT ON COLUMN digital_human_audio.status IS '状态:0生成中/1成功/2失败'; +COMMENT ON COLUMN digital_human_audio.error_msg IS '错误信息'; +COMMENT ON COLUMN digital_human_audio.duration IS '音频时长(秒)'; +COMMENT ON COLUMN digital_human_audio.external_id IS '外部音频ID'; +COMMENT ON COLUMN digital_human_audio.voice IS '音色:serena/vivian/uncle_fu/ryan/aiden/ono_anna/sohee/eric/dylan'; +COMMENT ON COLUMN digital_human_audio.voice_type IS '音色类型:preset/custom(预设/克隆)'; +COMMENT ON COLUMN digital_human_audio.custom_voice IS '自定义音色ID'; + +--------------------pgsql创建digital_human_custom_voice表语句--------------------------- +-- 自定义音色表 +CREATE TABLE IF NOT EXISTS digital_human_custom_voice ( + -- 基础字段(继承 SQLBaseCol 通用字段) + id BIGINT PRIMARY KEY, -- 主键ID(非自增) + tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8类型 + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 音色核心字段 + name VARCHAR(128) NOT NULL, -- 音色名称 + description TEXT DEFAULT '', -- 音色描述 + text TEXT DEFAULT '', -- 参考文本 + reference_audio BYTEA -- 参考音频数据(二进制) +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_custom_voice_tenant_id ON digital_human_custom_voice(tenant_id); +CREATE INDEX IF NOT EXISTS idx_custom_voice_name ON digital_human_custom_voice(name); +CREATE INDEX IF NOT EXISTS idx_custom_voice_deleted_at ON digital_human_custom_voice(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE digital_human_custom_voice IS '自定义音色表'; +COMMENT ON COLUMN digital_human_custom_voice.id IS '主键ID(非自增)'; +COMMENT ON COLUMN digital_human_custom_voice.tenant_id IS '租户ID'; +COMMENT ON COLUMN digital_human_custom_voice.creator IS '创建人'; +COMMENT ON COLUMN digital_human_custom_voice.created_at IS '创建时间'; +COMMENT ON COLUMN digital_human_custom_voice.updater IS '更新人'; +COMMENT ON COLUMN digital_human_custom_voice.updated_at IS '更新时间'; +COMMENT ON COLUMN digital_human_custom_voice.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN digital_human_custom_voice.name IS '音色名称'; +COMMENT ON COLUMN digital_human_custom_voice.description IS '音色描述'; +COMMENT ON COLUMN digital_human_custom_voice.text IS '参考文本'; +COMMENT ON COLUMN digital_human_custom_voice.reference_audio IS '参考音频数据(二进制)'; + +-- 兼容已有库:自定义音色增加状态/结果字段(对接异步模型服务) +ALTER TABLE digital_human_custom_voice ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0; +ALTER TABLE digital_human_custom_voice ADD COLUMN IF NOT EXISTS error_msg TEXT DEFAULT ''; +ALTER TABLE digital_human_custom_voice ADD COLUMN IF NOT EXISTS oss_file VARCHAR(512) DEFAULT ''; +COMMENT ON COLUMN digital_human_custom_voice.status IS '状态:0生成中/1成功/2失败'; +COMMENT ON COLUMN digital_human_custom_voice.error_msg IS '错误信息'; +COMMENT ON COLUMN digital_human_custom_voice.oss_file IS '结果文件URL(如参考音频/特征文件等)'; +CREATE INDEX IF NOT EXISTS idx_custom_voice_status ON digital_human_custom_voice(status); + +--------------------pgsql创建digital_human_video表语句--------------------------- +-- 视频表 +CREATE TABLE IF NOT EXISTS digital_human_video ( + -- 基础字段 + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 视频核心字段 + name VARCHAR(128) NOT NULL, -- 视频名称 + description TEXT DEFAULT '', -- 视频描述 + audio_id BIGINT, -- 关联音频ID + script_text TEXT NOT NULL, -- 话术文本 + video_url VARCHAR(512) DEFAULT '', -- 视频文件URL + status SMALLINT NOT NULL DEFAULT 0, -- 状态:0生成中/1成功/2失败 + error_msg TEXT DEFAULT '', -- 错误信息 + duration INT DEFAULT 0, -- 视频时长(秒) + thumbnail_url VARCHAR(512) DEFAULT '', -- 缩略图URL + external_id VARCHAR(64) DEFAULT '', -- 外部视频ID + digital_human_id BIGINT DEFAULT 0, -- 数字人ID(雪花算法ID) + digital_human_name VARCHAR(128) DEFAULT '' -- 数字人名称(冗余字段) +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_video_tenant_id ON digital_human_video(tenant_id); +CREATE INDEX IF NOT EXISTS idx_video_audio_id ON digital_human_video(audio_id); +CREATE INDEX IF NOT EXISTS idx_video_status ON digital_human_video(status); +CREATE INDEX IF NOT EXISTS idx_video_deleted_at ON digital_human_video(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE digital_human_video IS '视频表'; +COMMENT ON COLUMN digital_human_video.id IS '主键ID(非自增)'; +COMMENT ON COLUMN digital_human_video.tenant_id IS '租户ID'; +COMMENT ON COLUMN digital_human_video.audio_id IS '关联音频ID'; +COMMENT ON COLUMN digital_human_video.name IS '视频名称'; +COMMENT ON COLUMN digital_human_video.description IS '视频描述'; +COMMENT ON COLUMN digital_human_video.script_text IS '话术文本'; +COMMENT ON COLUMN digital_human_video.video_url IS '视频文件URL'; +COMMENT ON COLUMN digital_human_video.status IS '状态:0生成中/1成功/2失败'; +COMMENT ON COLUMN digital_human_video.error_msg IS '错误信息'; +COMMENT ON COLUMN digital_human_video.duration IS '视频时长(秒)'; +COMMENT ON COLUMN digital_human_video.thumbnail_url IS '缩略图URL'; +COMMENT ON COLUMN digital_human_video.external_id IS '外部视频ID'; +COMMENT ON COLUMN digital_human_video.digital_human_id IS '数字人ID'; +COMMENT ON COLUMN digital_human_video.digital_human_name IS '数字人名称(冗余字段)'; + +--------------------pgsql创建digital_human表语句--------------------------- +-- 数字人表 +CREATE TABLE IF NOT EXISTS digital_human ( + -- 基础字段 + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 数字人核心字段 + name VARCHAR(128) NOT NULL, -- 数字人名称 + description TEXT DEFAULT '', -- 数字人描述 + avatar_url VARCHAR(512) DEFAULT '', -- 头像URL + video_url VARCHAR(512) DEFAULT '', -- 形象视频URL + voice VARCHAR(32) DEFAULT 'serena', -- 默认音色 + status SMALLINT NOT NULL DEFAULT 1 -- 状态:1启用/0停用 +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_digital_human_tenant_id ON digital_human(tenant_id); +CREATE INDEX IF NOT EXISTS idx_digital_human_name ON digital_human(name); +CREATE INDEX IF NOT EXISTS idx_digital_human_status ON digital_human(status); +CREATE INDEX IF NOT EXISTS idx_digital_human_deleted_at ON digital_human(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE digital_human IS '数字人表'; +COMMENT ON COLUMN digital_human.id IS '主键ID(非自增)'; +COMMENT ON COLUMN digital_human.tenant_id IS '租户ID'; +COMMENT ON COLUMN digital_human.creator IS '创建人'; +COMMENT ON COLUMN digital_human.created_at IS '创建时间'; +COMMENT ON COLUMN digital_human.updater IS '更新人'; +COMMENT ON COLUMN digital_human.updated_at IS '更新时间'; +COMMENT ON COLUMN digital_human.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN digital_human.name IS '数字人名称'; +COMMENT ON COLUMN digital_human.description IS '数字人描述'; +COMMENT ON COLUMN digital_human.avatar_url IS '头像URL'; +COMMENT ON COLUMN digital_human.video_url IS '形象视频URL'; +COMMENT ON COLUMN digital_human.voice IS '默认音色'; +COMMENT ON COLUMN digital_human.status IS '状态:1启用/0停用'; + +--------------------pgsql创建digital_human_async_task_ref表语句--------------------------- +-- 异步任务绑定表(task_id -> 业务表+业务ID) +CREATE TABLE IF NOT EXISTS digital_human_async_task_ref ( + -- 基础字段 + id BIGINT PRIMARY KEY, -- 主键ID(非自增) + tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 绑定字段 + task_id VARCHAR(64) NOT NULL, -- 异步任务ID(model-asynch) + state SMALLINT NOT NULL DEFAULT 0, -- 任务状态(与 model-asynch 对齐:0/1/2/3/4) + table_name VARCHAR(64) NOT NULL, -- 业务表名:digital_human_audio / digital_human_custom_voice + biz_id BIGINT NOT NULL, -- 业务表主键ID(audio/custom_voice 的 id) + oss_file VARCHAR(512) DEFAULT '', -- 已转移后的业务侧OSS地址(可选) + error_msg TEXT DEFAULT '' -- 错误信息(可选) +); + +-- 索引 +CREATE UNIQUE INDEX IF NOT EXISTS uk_async_task_ref_tenant_task_id ON digital_human_async_task_ref(tenant_id, task_id); +CREATE INDEX IF NOT EXISTS idx_async_task_ref_tenant_state ON digital_human_async_task_ref(tenant_id, state); +CREATE INDEX IF NOT EXISTS idx_async_task_ref_table_biz ON digital_human_async_task_ref(table_name, biz_id); +CREATE INDEX IF NOT EXISTS idx_async_task_ref_deleted_at ON digital_human_async_task_ref(deleted_at); + +-- 注释 +COMMENT ON TABLE digital_human_async_task_ref IS '异步任务绑定表(task_id -> 业务表+业务ID)'; +COMMENT ON COLUMN digital_human_async_task_ref.task_id IS '异步任务ID(model-asynch)'; +COMMENT ON COLUMN digital_human_async_task_ref.state IS '任务状态(与 model-asynch 对齐:0/1/2/3/4)'; +COMMENT ON COLUMN digital_human_async_task_ref.table_name IS '业务表名'; +COMMENT ON COLUMN digital_human_async_task_ref.biz_id IS '业务表主键ID'; +COMMENT ON COLUMN digital_human_async_task_ref.oss_file IS '已转移后的业务侧OSS地址'; +COMMENT ON COLUMN digital_human_async_task_ref.error_msg IS '错误信息';