引包目录名调整
This commit is contained in:
@@ -1,195 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package consts
|
||||
|
||||
// Age 年龄段类型
|
||||
type Age string
|
||||
|
||||
// 年龄段常量
|
||||
const (
|
||||
AgeChild Age = "child" // 儿童
|
||||
AgeTeenager Age = "teenager" // 青少年
|
||||
AgeYoung Age = "young" // 青年
|
||||
AgeMiddle Age = "middle" // 中年
|
||||
AgeSenior Age = "senior" // 老年
|
||||
AgeUnlimited Age = "unlimited" // 不限
|
||||
)
|
||||
|
||||
// GetAgeText 获取年龄段文本
|
||||
func GetAgeText(age string) string {
|
||||
switch age {
|
||||
case string(AgeChild):
|
||||
return "儿童"
|
||||
case string(AgeTeenager):
|
||||
return "青少年"
|
||||
case string(AgeYoung):
|
||||
return "青年"
|
||||
case string(AgeMiddle):
|
||||
return "中年"
|
||||
case string(AgeSenior):
|
||||
return "老年"
|
||||
case string(AgeUnlimited):
|
||||
return "不限"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllAgeKeyValue 获取所有年龄段选项
|
||||
func GetAllAgeKeyValue() []AgeKeyValue {
|
||||
return []AgeKeyValue{
|
||||
{Value: string(AgeChild), Label: "儿童"},
|
||||
{Value: string(AgeTeenager), Label: "青少年"},
|
||||
{Value: string(AgeYoung), Label: "青年"},
|
||||
{Value: string(AgeMiddle), Label: "中年"},
|
||||
{Value: string(AgeSenior), Label: "老年"},
|
||||
{Value: string(AgeUnlimited), Label: "不限"},
|
||||
}
|
||||
}
|
||||
|
||||
// AgeKeyValue 年龄段键值对
|
||||
type AgeKeyValue struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package consts
|
||||
|
||||
// AudioStatus 音频状态类型
|
||||
type AudioStatus int
|
||||
|
||||
// 音频状态常量
|
||||
const (
|
||||
AudioStatusGenerating AudioStatus = 0 // 生成中
|
||||
AudioStatusSuccess AudioStatus = 1 // 成功
|
||||
AudioStatusFailed AudioStatus = 2 // 失败
|
||||
)
|
||||
|
||||
// GetAudioStatusText 获取音频状态文本
|
||||
func GetAudioStatusText(status int) string {
|
||||
switch status {
|
||||
case int(AudioStatusGenerating):
|
||||
return "生成中"
|
||||
case int(AudioStatusSuccess):
|
||||
return "成功"
|
||||
case int(AudioStatusFailed):
|
||||
return "失败"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllAudioStatusKeyValue 获取所有音频状态选项
|
||||
func GetAllAudioStatusKeyValue() []AudioStatusKeyValue {
|
||||
return []AudioStatusKeyValue{
|
||||
{Value: int(AudioStatusGenerating), Label: "生成中"},
|
||||
{Value: int(AudioStatusSuccess), Label: "成功"},
|
||||
{Value: int(AudioStatusFailed), Label: "失败"},
|
||||
}
|
||||
}
|
||||
|
||||
// AudioStatusKeyValue 音频状态键值对
|
||||
type AudioStatusKeyValue struct {
|
||||
Value int `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package consts
|
||||
|
||||
// MongoDB集合名称常量
|
||||
const (
|
||||
DigitalHumanCollection = "digital_human" // 数字人形象集合
|
||||
AudioCollection = "audio" // 音频集合
|
||||
VideoCollection = "video" // 视频集合
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
package consts
|
||||
|
||||
// CustomVoiceStatus 自定义音色状态类型
|
||||
type CustomVoiceStatus int
|
||||
|
||||
const (
|
||||
CustomVoiceStatusGenerating CustomVoiceStatus = 0 // 生成中
|
||||
CustomVoiceStatusSuccess CustomVoiceStatus = 1 // 成功
|
||||
CustomVoiceStatusFailed CustomVoiceStatus = 2 // 失败
|
||||
)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package consts
|
||||
|
||||
// DigitalHumanStatus 数字人状态类型
|
||||
type DigitalHumanStatus int
|
||||
|
||||
// 数字人状态常量
|
||||
const (
|
||||
DigitalHumanStatusInactive DigitalHumanStatus = 0 // 停用
|
||||
DigitalHumanStatusActive DigitalHumanStatus = 1 // 启用
|
||||
)
|
||||
|
||||
// GetDigitalHumanStatusText 获取数字人状态文本
|
||||
func GetDigitalHumanStatusText(status int) string {
|
||||
switch status {
|
||||
case int(DigitalHumanStatusInactive):
|
||||
return "停用"
|
||||
case int(DigitalHumanStatusActive):
|
||||
return "启用"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllDigitalHumanStatusKeyValue 获取所有数字人状态选项
|
||||
func GetAllDigitalHumanStatusKeyValue() []DigitalHumanStatusKeyValue {
|
||||
return []DigitalHumanStatusKeyValue{
|
||||
{Value: int(DigitalHumanStatusInactive), Label: "停用"},
|
||||
{Value: int(DigitalHumanStatusActive), Label: "启用"},
|
||||
}
|
||||
}
|
||||
|
||||
// DigitalHumanStatusKeyValue 数字人状态键值对
|
||||
type DigitalHumanStatusKeyValue struct {
|
||||
Value int `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// GetStatusText 获取状态文本(向后兼容)
|
||||
func GetStatusText(status int) string {
|
||||
return GetDigitalHumanStatusText(status)
|
||||
}
|
||||
|
||||
// GetAllStatusKeyValue 获取所有状态选项(向后兼容)
|
||||
func GetAllStatusKeyValue() []StatusKeyValue {
|
||||
return []StatusKeyValue{
|
||||
{Value: int(DigitalHumanStatusInactive), Label: "停用"},
|
||||
{Value: int(DigitalHumanStatusActive), Label: "启用"},
|
||||
}
|
||||
}
|
||||
|
||||
// StatusKeyValue 状态键值对(向后兼容)
|
||||
type StatusKeyValue struct {
|
||||
Value int `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package consts
|
||||
|
||||
// Gender 性别类型
|
||||
type Gender string
|
||||
|
||||
// 性别常量
|
||||
const (
|
||||
GenderMale Gender = "male" // 男
|
||||
GenderFemale Gender = "female" // 女
|
||||
GenderOther Gender = "other" // 其他
|
||||
)
|
||||
|
||||
// GetGenderText 获取性别文本
|
||||
func GetGenderText(gender string) string {
|
||||
switch gender {
|
||||
case string(GenderMale):
|
||||
return "男"
|
||||
case string(GenderFemale):
|
||||
return "女"
|
||||
case string(GenderOther):
|
||||
return "其他"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllGenderKeyValue 获取所有性别选项
|
||||
func GetAllGenderKeyValue() []GenderKeyValue {
|
||||
return []GenderKeyValue{
|
||||
{Value: string(GenderMale), Label: "男"},
|
||||
{Value: string(GenderFemale), Label: "女"},
|
||||
{Value: string(GenderOther), Label: "其他"},
|
||||
}
|
||||
}
|
||||
|
||||
// GenderKeyValue 性别键值对
|
||||
type GenderKeyValue struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package public
|
||||
|
||||
const (
|
||||
ModelNameCustomVoice = "qwen3-tts-customvoice" // 预设音频
|
||||
ModelNameVoiceDesign = "qwen3-tts-voicedesign" // 设计音频
|
||||
ModelNameBase = "qwen3-tts-base" // 克隆音频
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package public
|
||||
|
||||
const (
|
||||
TableNameAudio = "digital_human_audio"
|
||||
TableNameCustomVoice = "digital_human_custom_voice"
|
||||
TableNameAsyncTaskRef = "digital_human_async_task_ref"
|
||||
TableNameVideo = "digital_human_video"
|
||||
TableNameDigitalHuman = "digital_human"
|
||||
)
|
||||
@@ -1,51 +0,0 @@
|
||||
package consts
|
||||
|
||||
// Resolution 视频分辨率
|
||||
type Resolution string
|
||||
|
||||
const (
|
||||
Resolution480P Resolution = "480p" // 标清
|
||||
Resolution720P Resolution = "720p" // 高清
|
||||
Resolution1080P Resolution = "1080p" // 全高清
|
||||
Resolution2K Resolution = "2k" // 2K超清
|
||||
Resolution4K Resolution = "4k" // 4K超高清
|
||||
Resolution8K Resolution = "8k" // 8K超高清
|
||||
)
|
||||
|
||||
// Text 获取分辨率文本描述
|
||||
func (r Resolution) Text() string {
|
||||
switch r {
|
||||
case Resolution480P:
|
||||
return "标清 (480p)"
|
||||
case Resolution720P:
|
||||
return "高清 (720p)"
|
||||
case Resolution1080P:
|
||||
return "全高清 (1080p)"
|
||||
case Resolution2K:
|
||||
return "2K超清 (1440p)"
|
||||
case Resolution4K:
|
||||
return "4K超高清 (2160p)"
|
||||
case Resolution8K:
|
||||
return "8K超高清 (4320p)"
|
||||
default:
|
||||
return string(r)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolutionKeyValue 分辨率键值对(用于前端选项)
|
||||
type ResolutionKeyValue struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// GetResolutionOptions 获取所有分辨率选项
|
||||
func GetResolutionOptions() []ResolutionKeyValue {
|
||||
return []ResolutionKeyValue{
|
||||
{Key: string(Resolution480P), Value: Resolution480P.Text()},
|
||||
{Key: string(Resolution720P), Value: Resolution720P.Text()},
|
||||
{Key: string(Resolution1080P), Value: Resolution1080P.Text()},
|
||||
{Key: string(Resolution2K), Value: Resolution2K.Text()},
|
||||
{Key: string(Resolution4K), Value: Resolution4K.Text()},
|
||||
{Key: string(Resolution8K), Value: Resolution8K.Text()},
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package consts
|
||||
|
||||
// Style 风格类型
|
||||
type Style string
|
||||
|
||||
// 风格常量
|
||||
const (
|
||||
StyleBusiness Style = "business" // 商务
|
||||
StyleCasual Style = "casual" // 休闲
|
||||
StyleFormal Style = "formal" // 正式
|
||||
StyleCreative Style = "creative" // 创意
|
||||
StyleElegant Style = "elegant" // 优雅
|
||||
StyleFriendly Style = "friendly" // 友好
|
||||
StyleProfessional Style = "professional" // 专业
|
||||
StyleUnlimited Style = "unlimited" // 不限
|
||||
)
|
||||
|
||||
// GetStyleText 获取风格文本
|
||||
func GetStyleText(style string) string {
|
||||
switch style {
|
||||
case string(StyleBusiness):
|
||||
return "商务"
|
||||
case string(StyleCasual):
|
||||
return "休闲"
|
||||
case string(StyleFormal):
|
||||
return "正式"
|
||||
case string(StyleCreative):
|
||||
return "创意"
|
||||
case string(StyleElegant):
|
||||
return "优雅"
|
||||
case string(StyleFriendly):
|
||||
return "友好"
|
||||
case string(StyleProfessional):
|
||||
return "专业"
|
||||
case string(StyleUnlimited):
|
||||
return "不限"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllStyleKeyValue 获取所有风格选项
|
||||
func GetAllStyleKeyValue() []StyleKeyValue {
|
||||
return []StyleKeyValue{
|
||||
{Value: string(StyleBusiness), Label: "商务"},
|
||||
{Value: string(StyleCasual), Label: "休闲"},
|
||||
{Value: string(StyleFormal), Label: "正式"},
|
||||
{Value: string(StyleCreative), Label: "创意"},
|
||||
{Value: string(StyleElegant), Label: "优雅"},
|
||||
{Value: string(StyleFriendly), Label: "友好"},
|
||||
{Value: string(StyleProfessional), Label: "专业"},
|
||||
{Value: string(StyleUnlimited), Label: "不限"},
|
||||
}
|
||||
}
|
||||
|
||||
// StyleKeyValue 风格键值对
|
||||
type StyleKeyValue struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package consts
|
||||
|
||||
// VideoStatus 视频状态类型
|
||||
type VideoStatus int
|
||||
|
||||
// 视频生成状态常量
|
||||
const (
|
||||
VideoStatusGenerating VideoStatus = 0 // 生成中
|
||||
VideoStatusSuccess VideoStatus = 1 // 成功
|
||||
VideoStatusFailed VideoStatus = 2 // 失败
|
||||
)
|
||||
|
||||
// GetVideoStatusText 获取视频状态文本
|
||||
func GetVideoStatusText(status int) string {
|
||||
switch status {
|
||||
case int(VideoStatusGenerating):
|
||||
return "生成中"
|
||||
case int(VideoStatusSuccess):
|
||||
return "成功"
|
||||
case int(VideoStatusFailed):
|
||||
return "失败"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllVideoStatusKeyValue 获取所有视频状态选项
|
||||
func GetAllVideoStatusKeyValue() []VideoStatusKeyValue {
|
||||
return []VideoStatusKeyValue{
|
||||
{Value: int(VideoStatusGenerating), Label: "生成中"},
|
||||
{Value: int(VideoStatusSuccess), Label: "成功"},
|
||||
{Value: int(VideoStatusFailed), Label: "失败"},
|
||||
}
|
||||
}
|
||||
|
||||
// VideoStatusKeyValue 视频状态键值对
|
||||
type VideoStatusKeyValue struct {
|
||||
Value int `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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()
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"digital-human/digitalhuman/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编码的音频数据
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"digital-human/digitalhuman/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:"风格选项列表"`
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"digital-human/digitalhuman/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:"分辨率选项列表"`
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"digital-human/digitalhuman/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(用于声音克隆)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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"` // 参考音频数据(二进制)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"digital-human/digitalhuman/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"` // 动态元数据
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"digital-human/digitalhuman/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"` // 分辨率
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/dao"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/dao"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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 创建异步任务并通过轮询/批量领取获取结果")
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"digital-human/digitalhuman/consts/public"
|
||||
"digital-human/digitalhuman/dao"
|
||||
"digital-human/digitalhuman/model/dto"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/dao"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"digital-human/digitalhuman/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 暂未实现:后续接入语音识别模型后补齐")
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"digital-human/digitalhuman/consts"
|
||||
"digital-human/digitalhuman/dao"
|
||||
"digital-human/digitalhuman/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
|
||||
}
|
||||
Reference in New Issue
Block a user