86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
)
|
|
|
|
const (
|
|
renderFramesCount = 36
|
|
renderFrameSize = "256x512"
|
|
renderTimeout = 15 * time.Minute
|
|
)
|
|
|
|
// RenderAvatarFrames 将化身 GLB 预渲染为绕 Y 轴旋转帧序列,返回帧目录访问 URL。
|
|
// 帧目录已就绪直接复用;render.enabled=false 或渲染失败时返回 error,由调用方降级。
|
|
func RenderAvatarFrames(ctx context.Context, glb string, outKey string) (framesURL string, err error) {
|
|
if !g.Cfg().MustGet(ctx, "render.enabled", true).Bool() {
|
|
return "", errors.New("3D 渲染服务未启用")
|
|
}
|
|
dir := filepath.Join("workspace", "avatar_frames", outKey)
|
|
if framesReady(dir) {
|
|
return "/workspace/avatar_frames/" + outKey, nil
|
|
}
|
|
if _, err := os.Stat(glb); err != nil {
|
|
return "", fmt.Errorf("化身 GLB 不存在: %w", err)
|
|
}
|
|
if err := runRender(ctx, glb, dir); err != nil {
|
|
return "", err
|
|
}
|
|
return "/workspace/avatar_frames/" + outKey, nil
|
|
}
|
|
|
|
func framesReady(dir string) bool {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
count := 0
|
|
for _, e := range entries {
|
|
if strings.HasPrefix(e.Name(), "frame_") && strings.HasSuffix(e.Name(), ".png") {
|
|
count++
|
|
}
|
|
}
|
|
return count >= renderFramesCount
|
|
}
|
|
|
|
func nodeBin(ctx context.Context) string {
|
|
bin := g.Cfg().MustGet(ctx, "render.node_bin", "node").String()
|
|
if bin == "" {
|
|
return "node"
|
|
}
|
|
// 配置路径不存在(如容器环境)→ 回退 PATH 中的 node
|
|
if _, err := os.Stat(bin); err != nil {
|
|
return "node"
|
|
}
|
|
return bin
|
|
}
|
|
|
|
func runRender(ctx context.Context, glb, out string) error {
|
|
if err := os.MkdirAll(out, 0o755); err != nil {
|
|
return err
|
|
}
|
|
return execNode(ctx, filepath.Join("scripts", "avatar-render", "render.js"),
|
|
"--glb", glb, "--out", out,
|
|
"--frames", fmt.Sprint(renderFramesCount), "--size", renderFrameSize)
|
|
}
|
|
|
|
func execNode(ctx context.Context, script string, args ...string) error {
|
|
cmdCtx, cancel := context.WithTimeout(ctx, renderTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(cmdCtx, nodeBin(ctx), append([]string{script}, args...)...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("node 渲染失败: %v: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|