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 }