219 lines
6.8 KiB
Go
219 lines
6.8 KiB
Go
package service
|
||
|
||
import (
|
||
"ai-agent/digital-human/consts"
|
||
"ai-agent/digital-human/dao"
|
||
"ai-agent/digital-human/model/dto"
|
||
"context"
|
||
"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
|
||
}
|