Files
ai-agent/digital-human/service/video_service.go
2026-04-27 14:02:43 +08:00

219 lines
6.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
// 保存视频IDPostgreSQL 使用 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
}