1967 lines
84 KiB
Go
1967 lines
84 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
|
||
"yingce/backend/internal/model"
|
||
"yingce/backend/internal/repository"
|
||
)
|
||
|
||
const (
|
||
cloudAgentPrevisMaxOperations = 32
|
||
cloudAgentPrevisMaxScenes = 32
|
||
cloudAgentPrevisMaxShots = 64
|
||
cloudAgentPrevisMaxObjects = 128
|
||
cloudAgentPrevisMaxCameras = 32
|
||
cloudAgentPrevisMaxLights = 64
|
||
cloudAgentPrevisDocumentLimit = 1 << 20
|
||
cloudAgentPrevisTextLimit = 160
|
||
cloudAgentPrevisWorkstationWidth = 520.0
|
||
cloudAgentPrevisWorkstationHeight = 340.0
|
||
)
|
||
|
||
type cloudAgentPrevisSceneCreateArgs struct {
|
||
CanvasSnapshotHash string `json:"canvasSnapshotHash"`
|
||
SceneID string `json:"sceneId"`
|
||
Title string `json:"title"`
|
||
TemplateID string `json:"templateId"`
|
||
NodeID string `json:"nodeId,omitempty"`
|
||
NodeTitle string `json:"nodeTitle,omitempty"`
|
||
}
|
||
|
||
type cloudAgentPrevisApplyPatchArgs struct {
|
||
SnapshotHash string `json:"snapshotHash"`
|
||
SceneID string `json:"sceneId"`
|
||
Operations []cloudAgentPrevisPatchOperation `json:"operations"`
|
||
}
|
||
|
||
// cloudAgentPrevisPatchOperation is deliberately a closed semantic contract.
|
||
// It has no generic path, metadata, URL, storage key or raw scene field.
|
||
type cloudAgentPrevisPatchOperation struct {
|
||
Type string `json:"type"`
|
||
ID string `json:"id"`
|
||
Name *string `json:"name"`
|
||
Title *string `json:"title"`
|
||
Kind string `json:"kind"`
|
||
Primitive string `json:"primitive"`
|
||
Archetype string `json:"archetype"` // adult/child/elderly/man/woman/monster
|
||
Color *string `json:"color"`
|
||
ParentID *string `json:"parentId"`
|
||
Visible *bool `json:"visible"`
|
||
Pose string `json:"pose"`
|
||
|
||
CharacterAssetID *string `json:"characterAssetId"`
|
||
CharacterVersionID *string `json:"characterVersionId"`
|
||
ReferenceNodeID *string `json:"referenceNodeId"`
|
||
CharacterName *string `json:"characterName"`
|
||
|
||
Position *[]float64 `json:"position"`
|
||
Rotation *[]float64 `json:"rotation"`
|
||
Scale *[]float64 `json:"scale"`
|
||
Target *[]float64 `json:"target"`
|
||
|
||
KeyframeID *string `json:"keyframeId"`
|
||
Time *float64 `json:"time"`
|
||
Easing string `json:"easing"`
|
||
PathPoints [][]float64 `json:"pathPoints"`
|
||
PathSpeed string `json:"pathSpeed"`
|
||
StartDelay *float64 `json:"startDelay"`
|
||
OrientToPath *bool `json:"orientToPath"`
|
||
|
||
CameraID *string `json:"cameraId"`
|
||
Duration *float64 `json:"duration"`
|
||
FPS *int `json:"fps"`
|
||
ShotSize string `json:"shotSize"`
|
||
CameraMove string `json:"cameraMove"`
|
||
Prompt *string `json:"prompt"`
|
||
FocalLength *float64 `json:"focalLength"`
|
||
|
||
LightType *string `json:"lightType"`
|
||
Intensity *float64 `json:"intensity"`
|
||
Angle *float64 `json:"angle"`
|
||
Penumbra *float64 `json:"penumbra"`
|
||
CastShadow *bool `json:"castShadow"`
|
||
|
||
Background *string `json:"background"`
|
||
EnvironmentIntensity *float64 `json:"environmentIntensity"`
|
||
GridVisible *bool `json:"gridVisible"`
|
||
ActiveShotID *string `json:"activeShotId"`
|
||
}
|
||
|
||
type cloudAgentPrevisMutationPlan struct {
|
||
Canvas *model.CanvasProject
|
||
Document map[string]any
|
||
BeforeJSON string
|
||
BeforeSnapshotHash string
|
||
AfterSnapshotHash string
|
||
ResultSnapshotHash string
|
||
Operation string
|
||
Preview cloudAgentApprovalPreview
|
||
SceneID string
|
||
NodeID string
|
||
ShotID string
|
||
}
|
||
|
||
func cloudAgentPrevisSceneCreateSchema() map[string]any {
|
||
return map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"canvasSnapshotHash": map[string]any{"type": "string", "description": "previs_scene_read 场景目录返回的 canvasSnapshotHash;创建前画布必须未变化"},
|
||
"sceneId": map[string]any{"type": "string", "maxLength": 80, "description": "新的稳定场景 ID"},
|
||
"title": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit, "description": "场景标题"},
|
||
"templateId": map[string]any{"type": "string", "enum": []string{"empty", "monologue", "dialogue", "blocking", "product", "action_chase", "interior", "crowd"}, "description": "模板:empty/monologue/dialogue/blocking/product/action_chase/interior/crowd"},
|
||
"nodeId": map[string]any{"type": "string", "maxLength": 80, "description": "可选的预演视频工作站节点 ID;省略时由 sceneId 派生稳定 ID"},
|
||
"nodeTitle": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit, "description": "可选的预演视频工作站节点标题"},
|
||
},
|
||
"required": []string{"canvasSnapshotHash", "sceneId", "title", "templateId"},
|
||
"additionalProperties": false,
|
||
}
|
||
}
|
||
|
||
func cloudAgentPrevisPatchOperationSchema() map[string]any {
|
||
vec3 := map[string]any{"type": "array", "minItems": 3, "maxItems": 3, "items": map[string]any{"type": "number"}}
|
||
properties := map[string]any{
|
||
"type": map[string]any{"type": "string", "enum": []string{"scene_update", "shot_add", "shot_update", "shot_remove", "object_add", "object_update", "object_remove", "object_keyframe_upsert", "object_keyframe_remove", "object_motion_path_set", "object_motion_path_clear", "camera_add", "camera_update", "camera_remove", "camera_keyframe_upsert", "camera_keyframe_remove", "light_add", "light_update", "light_remove"}},
|
||
"id": map[string]any{"type": "string", "maxLength": 80, "description": "目标或新建资源 ID"},
|
||
"name": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit},
|
||
"title": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit},
|
||
"kind": map[string]any{"type": "string", "enum": []string{"primitive", "actor"}},
|
||
"primitive": map[string]any{"type": "string", "enum": []string{"box", "sphere", "cylinder", "plane", "character"}},
|
||
"archetype": map[string]any{"type": "string", "enum": []string{"adult", "child", "elderly", "man", "woman", "monster"}, "description": "演员体型原型"},
|
||
"characterAssetId": map[string]any{"type": "string", "maxLength": 120},
|
||
"characterVersionId": map[string]any{"type": "string", "maxLength": 120},
|
||
"referenceNodeId": map[string]any{"type": "string", "maxLength": 120},
|
||
"characterName": map[string]any{"type": "string", "maxLength": cloudAgentPrevisTextLimit},
|
||
"pose": map[string]any{"type": "string", "enum": []string{"stand", "sit", "walk", "run", "wave", "think", "fight", "kick", "throw", "push", "reach", "arms_crossed", "phone", "bow", "kneel_single", "kneel_double", "lean", "squat", "hands_hips", "neutral", "t_pose"}, "description": "演员姿态"},
|
||
"color": map[string]any{"type": "string", "maxLength": 9, "pattern": `^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$`, "description": "十六进制颜色"},
|
||
"parentId": map[string]any{"type": "string", "maxLength": 80},
|
||
"visible": map[string]any{"type": "boolean"},
|
||
|
||
"position": vec3,
|
||
"rotation": vec3,
|
||
"scale": vec3,
|
||
"target": vec3,
|
||
"keyframeId": map[string]any{"type": "string", "maxLength": 120},
|
||
"time": map[string]any{"type": "number", "minimum": 0, "maximum": 60},
|
||
"easing": map[string]any{"type": "string", "enum": []string{"step", "linear", "smooth"}},
|
||
"pathPoints": map[string]any{"type": "array", "minItems": 2, "maxItems": 64, "items": vec3},
|
||
"pathSpeed": map[string]any{"type": "string", "enum": []string{"slow", "walk", "run"}},
|
||
"startDelay": map[string]any{"type": "number", "minimum": 0, "maximum": 60},
|
||
"orientToPath": map[string]any{"type": "boolean"},
|
||
"cameraId": map[string]any{"type": "string", "maxLength": 80},
|
||
"duration": map[string]any{"type": "number", "minimum": 0.1, "maximum": 60},
|
||
"fps": map[string]any{"type": "integer", "minimum": 1, "maximum": 60},
|
||
"shotSize": map[string]any{"type": "string", "enum": []string{"extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"}},
|
||
"cameraMove": map[string]any{"type": "string", "enum": []string{"static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"}},
|
||
"prompt": map[string]any{"type": "string", "maxLength": 2000},
|
||
"focalLength": map[string]any{"type": "number", "minimum": 1, "maximum": 300},
|
||
"lightType": map[string]any{"type": "string", "enum": []string{"directional", "point", "spot", "ambient"}},
|
||
"intensity": map[string]any{"type": "number", "minimum": 0, "maximum": 100},
|
||
"angle": map[string]any{"type": "number", "minimum": 0, "maximum": 6.29},
|
||
"penumbra": map[string]any{"type": "number", "minimum": 0, "maximum": 1},
|
||
"castShadow": map[string]any{"type": "boolean"},
|
||
"background": map[string]any{"type": "string", "maxLength": 32},
|
||
"environmentIntensity": map[string]any{"type": "number", "minimum": 0, "maximum": 100},
|
||
"gridVisible": map[string]any{"type": "boolean"},
|
||
"activeShotId": map[string]any{"type": "string", "maxLength": 80},
|
||
}
|
||
return map[string]any{"type": "object", "properties": properties, "required": []string{"type", "id"}, "additionalProperties": false}
|
||
}
|
||
|
||
func cloudAgentPrevisApplyPatchSchema() map[string]any {
|
||
return map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"snapshotHash": map[string]any{"type": "string", "description": "previs_scene_read 聚焦读取返回的场景 snapshotHash;只代表该场景"},
|
||
"sceneId": map[string]any{"type": "string", "maxLength": 80, "description": "要修改的场景 ID"},
|
||
"operations": map[string]any{"type": "array", "minItems": 1, "maxItems": cloudAgentPrevisMaxOperations, "items": cloudAgentPrevisPatchOperationSchema(), "description": "受限语义操作;不能替换整个场景或写任意 JSON"},
|
||
},
|
||
"required": []string{"snapshotHash", "sceneId", "operations"},
|
||
"additionalProperties": false,
|
||
}
|
||
}
|
||
|
||
func cloudAgentPrevisValidateText(value, label string, maxRunes int, required bool) error {
|
||
if required && strings.TrimSpace(value) == "" {
|
||
return BadAuthRequest(label + "不能为空")
|
||
}
|
||
if !utf8.ValidString(value) || strings.TrimSpace(value) != value {
|
||
return BadAuthRequest(label + "不能包含首尾空白或无效字符")
|
||
}
|
||
if utf8.RuneCountInString(value) > maxRunes {
|
||
return fmt.Errorf("%s不能超过 %d 个字符", label, maxRunes)
|
||
}
|
||
for _, r := range value {
|
||
if unicode.IsControl(r) {
|
||
return BadAuthRequest(label + "不能包含控制字符")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// cloudAgentPrevisValidatePose validates pose against the canonical enum.
|
||
func cloudAgentPrevisValidatePose(pose string) error {
|
||
valid := map[string]bool{
|
||
"stand": true, "sit": true, "walk": true, "run": true, "wave": true, "think": true,
|
||
"fight": true, "kick": true, "throw": true, "push": true, "reach": true, "arms_crossed": true,
|
||
"phone": true, "bow": true, "kneel_single": true, "kneel_double": true, "lean": true,
|
||
"squat": true, "hands_hips": true, "neutral": true, "t_pose": true,
|
||
}
|
||
if !valid[pose] {
|
||
return BadAuthRequest(fmt.Sprintf("pose 值 %q 不受支持;请使用 stand/sit/walk/run/wave/think/fight 等", pose))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisValidateEnum(value, label string, allowed ...string) error {
|
||
for _, candidate := range allowed {
|
||
if value == candidate {
|
||
return nil
|
||
}
|
||
}
|
||
return BadAuthRequest(label + "不受支持")
|
||
}
|
||
|
||
func cloudAgentPrevisIsHexColor(value string) bool {
|
||
if len(value) != 4 && len(value) != 7 && len(value) != 9 || value[0] != '#' {
|
||
return false
|
||
}
|
||
for _, r := range value[1:] {
|
||
if !strings.ContainsRune("0123456789abcdefABCDEF", r) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func cloudAgentPrevisNormalizeLegacyColor(value string) string {
|
||
trimmed := strings.TrimSpace(value)
|
||
if cloudAgentPrevisIsHexColor(trimmed) {
|
||
return strings.ToLower(trimmed)
|
||
}
|
||
lower := strings.ToLower(trimmed)
|
||
var raw string
|
||
var rgba bool
|
||
switch {
|
||
case strings.HasPrefix(lower, "rgb(") && strings.HasSuffix(lower, ")"):
|
||
raw = trimmed[4 : len(trimmed)-1]
|
||
case strings.HasPrefix(lower, "rgba(") && strings.HasSuffix(lower, ")"):
|
||
raw = trimmed[5 : len(trimmed)-1]
|
||
rgba = true
|
||
default:
|
||
return value
|
||
}
|
||
parts := strings.Split(raw, ",")
|
||
if len(parts) != 3 && len(parts) != 4 {
|
||
return value
|
||
}
|
||
channels := make([]int, 3)
|
||
for index := range channels {
|
||
channel, err := strconv.Atoi(strings.TrimSpace(parts[index]))
|
||
if err != nil || channel < 0 || channel > 255 {
|
||
return value
|
||
}
|
||
channels[index] = channel
|
||
}
|
||
if !rgba && len(parts) == 4 {
|
||
return value
|
||
}
|
||
result := fmt.Sprintf("#%02x%02x%02x", channels[0], channels[1], channels[2])
|
||
if rgba && len(parts) == 4 {
|
||
alpha, err := strconv.ParseFloat(strings.TrimSpace(parts[3]), 64)
|
||
if err != nil {
|
||
return value
|
||
}
|
||
if alpha <= 1 {
|
||
alpha = math.Round(alpha * 255)
|
||
}
|
||
if alpha < 0 || alpha > 255 {
|
||
return value
|
||
}
|
||
result += fmt.Sprintf("%02x", int(alpha))
|
||
}
|
||
return result
|
||
}
|
||
|
||
// Legacy scenes can contain CSS rgb/rgba colors from the browser-era editor.
|
||
// Normalize those values before strict validation so a harmless scene read or
|
||
// patch is not blocked by historical presentation data.
|
||
func cloudAgentPrevisNormalizeSceneColor(value, fallback string) string {
|
||
normalized := cloudAgentPrevisNormalizeLegacyColor(value)
|
||
if cloudAgentPrevisIsHexColor(normalized) {
|
||
return normalized
|
||
}
|
||
if strings.TrimSpace(value) == "" {
|
||
return fallback
|
||
}
|
||
return value
|
||
}
|
||
|
||
func cloudAgentPrevisValidateColor(value, label string) error {
|
||
if err := cloudAgentPrevisValidateText(value, label, 32, true); err != nil {
|
||
return err
|
||
}
|
||
if !cloudAgentPrevisIsHexColor(value) {
|
||
return BadAuthRequest(label + "必须是十六进制颜色")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisFiniteInRange(value float64, label string, minimum, maximum float64) error {
|
||
if math.IsNaN(value) || math.IsInf(value, 0) || value < minimum || value > maximum {
|
||
return BadAuthRequest(label + "超出限制")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisValidateTemplateID(value string) error {
|
||
valid := map[string]bool{"empty": true, "monologue": true, "dialogue": true, "blocking": true, "product": true, "action_chase": true, "interior": true, "crowd": true}
|
||
if !valid[value] {
|
||
return BadAuthRequest("templateId 不受支持;有效值:empty/monologue/dialogue/blocking/product/action_chase/interior/crowd")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisValidateVec3(value []float64, label string) error {
|
||
if len(value) != 3 {
|
||
return BadAuthRequest(label + "必须是包含3个数字的数组")
|
||
}
|
||
for _, number := range value {
|
||
if math.IsNaN(number) || math.IsInf(number, 0) || math.Abs(number) > 100000 {
|
||
return BadAuthRequest(label + "包含无效或超出范围的数字")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisVec3(value []float64) []any {
|
||
return []any{value[0], value[1], value[2]}
|
||
}
|
||
|
||
func cloudAgentPrevisNumberVec3(value any) ([]float64, bool) {
|
||
result := make([]float64, 3)
|
||
switch items := value.(type) {
|
||
case []float64:
|
||
if len(items) != 3 {
|
||
return nil, false
|
||
}
|
||
copy(result, items)
|
||
case []any:
|
||
if len(items) != 3 {
|
||
return nil, false
|
||
}
|
||
for index, item := range items {
|
||
result[index] = numberValue(item, math.NaN())
|
||
}
|
||
default:
|
||
return nil, false
|
||
}
|
||
for _, number := range result {
|
||
if math.IsNaN(number) || math.IsInf(number, 0) {
|
||
return nil, false
|
||
}
|
||
}
|
||
return result, true
|
||
}
|
||
|
||
func cloudAgentPrevisOptionalVec3(value *[]float64, label string) error {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
return cloudAgentPrevisValidateVec3(*value, label)
|
||
}
|
||
|
||
func cloudAgentPrevisTextPointer(value *string, label string, maxRunes int) error {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
return cloudAgentPrevisValidateText(*value, label, maxRunes, false)
|
||
}
|
||
|
||
func cloudAgentPrevisCanvas(repo *repository.Repository, userID, canvasID string) (*model.CanvasProject, map[string]any, error) {
|
||
canvas, err := repo.CanvasProjectForUser(userID, canvasID)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
doc, err := creationDocument(canvas.PayloadJSON)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return canvas, doc, nil
|
||
}
|
||
|
||
func cloudAgentPrevisScenesSize(scenes []map[string]any) error {
|
||
encoded, err := json.Marshal(scenes)
|
||
if err != nil || len(encoded) > cloudAgentPrevisDocumentLimit {
|
||
return BadAuthRequest("预演台场景数据超过限制")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisIDSet(items []map[string]any, label string) (map[string]map[string]any, error) {
|
||
result := make(map[string]map[string]any, len(items))
|
||
for _, item := range items {
|
||
id := stringValue(item["id"])
|
||
if err := validateCloudAgentID(id, label, 80); err != nil {
|
||
return nil, err
|
||
}
|
||
if _, exists := result[id]; exists {
|
||
return nil, BadAuthRequest(label + "重复")
|
||
}
|
||
result[id] = item
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func cloudAgentPrevisFinite(value any) bool {
|
||
number := numberValue(value, math.NaN())
|
||
return !math.IsNaN(number) && !math.IsInf(number, 0)
|
||
}
|
||
|
||
func cloudAgentPrevisValidateTransform(value any, label string) error {
|
||
transform, ok := value.(map[string]any)
|
||
if !ok {
|
||
return BadAuthRequest(label + "缺少 transform")
|
||
}
|
||
for _, key := range []string{"position", "rotation", "scale"} {
|
||
items, ok := transform[key].([]any)
|
||
if !ok || len(items) != 3 {
|
||
return BadAuthRequest(label + "的 transform." + key + " 必须包含3个数字")
|
||
}
|
||
for _, item := range items {
|
||
if !cloudAgentPrevisFinite(item) {
|
||
return BadAuthRequest(label + "的 transform 包含无效数字")
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisValidateScene(scene map[string]any) error {
|
||
if err := validateCloudAgentID(stringValue(scene["id"]), "导演场景 ID", 80); err != nil {
|
||
return err
|
||
}
|
||
if numberValue(scene["version"], 0) != 1 {
|
||
return BadAuthRequest("预演台场景版本不受支持")
|
||
}
|
||
if err := cloudAgentPrevisValidateText(stringValue(scene["title"]), "场景标题", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
background := cloudAgentPrevisNormalizeSceneColor(stringValue(scene["background"]), "#111827")
|
||
scene["background"] = background
|
||
if err := cloudAgentPrevisValidateColor(stringValue(scene["background"]), "场景背景色"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(scene["environmentIntensity"], math.NaN()), "环境光强度", 0, 100); err != nil {
|
||
return err
|
||
}
|
||
objects := creationMaps(scene["objects"])
|
||
cameras := creationMaps(scene["cameras"])
|
||
lights := creationMaps(scene["lights"])
|
||
shots := creationMaps(scene["shots"])
|
||
if len(objects) > cloudAgentPrevisMaxObjects || len(cameras) > cloudAgentPrevisMaxCameras || len(lights) > cloudAgentPrevisMaxLights || len(shots) > cloudAgentPrevisMaxShots {
|
||
return BadAuthRequest("预演台场景对象、摄影机、灯光或镜头数量超过限制")
|
||
}
|
||
objectByID, err := cloudAgentPrevisIDSet(objects, "导演对象 ID")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cameraByID, err := cloudAgentPrevisIDSet(cameras, "导演摄影机 ID")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_, err = cloudAgentPrevisIDSet(lights, "导演灯光 ID")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
shotByID, err := cloudAgentPrevisIDSet(shots, "导演镜头 ID")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(shots) == 0 || stringValue(scene["activeShotId"]) == "" || shotByID[stringValue(scene["activeShotId"])] == nil {
|
||
return BadAuthRequest("预演台场景必须包含有效的 activeShotId")
|
||
}
|
||
for id, object := range objectByID {
|
||
if err := cloudAgentPrevisValidateText(stringValue(object["name"]), "导演对象名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(stringValue(object["kind"]), "导演对象类型", "primitive", "model", "actor", "billboard"); err != nil {
|
||
return err
|
||
}
|
||
if primitive := stringValue(object["primitive"]); primitive != "" {
|
||
if err := cloudAgentPrevisValidateEnum(primitive, "导演对象 primitive", "box", "sphere", "cylinder", "plane", "character"); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
objectColor := cloudAgentPrevisNormalizeSceneColor(stringValue(object["color"]), "#8795a5")
|
||
object["color"] = objectColor
|
||
if err := cloudAgentPrevisValidateColor(stringValue(object["color"]), "导演对象颜色"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateTransform(object["transform"], "导演对象 "+id); err != nil {
|
||
return err
|
||
}
|
||
if binding, ok := object["characterBinding"].(map[string]any); ok {
|
||
for _, key := range []string{"characterAssetId", "characterVersionId", "referenceNodeId"} {
|
||
if err := validateCloudAgentID(stringValue(binding[key]), "角色绑定 "+key, 120); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if name := stringValue(binding["characterName"]); name != "" {
|
||
if err := cloudAgentPrevisValidateText(name, "角色绑定 characterName", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
if keyframes, ok := object["keyframes"]; ok {
|
||
for _, keyframe := range creationMaps(keyframes) {
|
||
if err := validateCloudAgentID(stringValue(keyframe["id"]), "对象关键帧 ID", 120); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(keyframe["time"], math.NaN()), "对象关键帧 time", 0, 60); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateTransform(keyframe["transform"], "对象关键帧 "+stringValue(keyframe["id"])); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
if motionPath, ok := object["motionPath"].(map[string]any); ok {
|
||
points, pointsOK := motionPath["points"].([]any)
|
||
if !pointsOK || len(points) < 2 || len(points) > 64 {
|
||
return BadAuthRequest("预演台 motionPath 点数无效")
|
||
}
|
||
for index, point := range points {
|
||
value, ok := cloudAgentPrevisNumberVec3(point)
|
||
if !ok {
|
||
return BadAuthRequest(fmt.Sprintf("预演台 motionPath 点 %d 无效", index))
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(value, "motionPath point"); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(stringValue(motionPath["speed"]), "motionPath speed", "slow", "walk", "run"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(motionPath["startDelay"], math.NaN()), "motionPath startDelay", 0, 60); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if parentID := stringValue(object["parentId"]); parentID != "" {
|
||
if err := validateCloudAgentID(parentID, "导演对象 parentId", 80); err != nil {
|
||
return err
|
||
}
|
||
if objectByID[parentID] == nil || parentID == id {
|
||
return BadAuthRequest("预演台对象 parentId 无效")
|
||
}
|
||
}
|
||
}
|
||
for id, camera := range cameraByID {
|
||
if err := cloudAgentPrevisValidateText(stringValue(camera["name"]), "导演摄影机名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(camera["focalLength"], math.NaN()), "摄影机焦段", 1, 300); err != nil {
|
||
return err
|
||
}
|
||
target, targetOK := cloudAgentPrevisNumberVec3(camera["target"])
|
||
if !targetOK {
|
||
return BadAuthRequest("导演摄影机 " + id + " target 必须包含3个数字")
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(target, "导演摄影机 "+id+" target"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateTransform(camera["transform"], "导演摄影机 "+id); err != nil {
|
||
return err
|
||
}
|
||
if keyframes, ok := camera["keyframes"]; ok {
|
||
for _, keyframe := range creationMaps(keyframes) {
|
||
if err := validateCloudAgentID(stringValue(keyframe["id"]), "摄影机关键帧 ID", 120); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(keyframe["time"], math.NaN()), "摄影机关键帧 time", 0, 60); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateTransform(keyframe["transform"], "摄影机关键帧 "+stringValue(keyframe["id"])); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for id, shot := range shotByID {
|
||
cameraID := stringValue(shot["cameraId"])
|
||
if cameraByID[cameraID] == nil {
|
||
return BadAuthRequest("导演镜头 " + id + " 引用了不存在的摄影机")
|
||
}
|
||
duration := numberValue(shot["duration"], math.NaN())
|
||
fps := numberValue(shot["fps"], math.NaN())
|
||
if err := cloudAgentPrevisFiniteInRange(duration, "导演镜头时长", 0.1, 60); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(fps, "导演镜头帧率", 1, 60); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateText(stringValue(shot["name"]), "导演镜头名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(stringValue(shot["shotSize"]), "导演景别", "extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(stringValue(shot["cameraMove"]), "导演运镜", "static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateText(stringValue(shot["prompt"]), "导演镜头提示词", 2000, false); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
for _, light := range creationMaps(scene["lights"]) {
|
||
lightID := stringValue(light["id"])
|
||
if err := cloudAgentPrevisValidateText(stringValue(light["name"]), "导演灯光名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(stringValue(light["type"]), "导演灯光类型", "directional", "point", "spot", "ambient"); err != nil {
|
||
return err
|
||
}
|
||
lightColor := cloudAgentPrevisNormalizeSceneColor(stringValue(light["color"]), "#ffffff")
|
||
light["color"] = lightColor
|
||
if err := cloudAgentPrevisValidateColor(stringValue(light["color"]), "导演灯光颜色"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(light["intensity"], math.NaN()), "导演灯光强度", 0, 100); err != nil {
|
||
return err
|
||
}
|
||
if angle, ok := light["angle"]; ok {
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(angle, math.NaN()), "导演灯光 "+lightID+" angle", 0, 6.29); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if penumbra, ok := light["penumbra"]; ok {
|
||
if err := cloudAgentPrevisFiniteInRange(numberValue(penumbra, math.NaN()), "导演灯光 "+lightID+" penumbra", 0, 1); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if err := cloudAgentPrevisValidateTransform(light["transform"], "导演灯光 "+lightID); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
// Parent links are a small directed graph. Walk every chain to reject cycles.
|
||
for id := range objectByID {
|
||
seen := map[string]bool{}
|
||
for current := id; current != ""; {
|
||
if seen[current] {
|
||
return BadAuthRequest("预演台对象 parentId 不能形成环")
|
||
}
|
||
seen[current] = true
|
||
current = stringValue(objectByID[current]["parentId"])
|
||
if current != "" && objectByID[current] == nil {
|
||
return BadAuthRequest("预演台对象 parentId 引用不存在对象")
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisSceneCreateTemplate(args cloudAgentPrevisSceneCreateArgs) (map[string]any, error) {
|
||
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cloudAgentPrevisValidateText(args.Title, "场景标题", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cloudAgentPrevisValidateTemplateID(args.TemplateID); err != nil {
|
||
return nil, err
|
||
}
|
||
now := time.Now().UTC().Format(time.RFC3339Nano)
|
||
cameraID := args.SceneID + "-camera-1"
|
||
shotID := args.SceneID + "-shot-1"
|
||
camera := map[string]any{
|
||
"id": cameraID, "name": "主摄影机",
|
||
"transform": map[string]any{"position": []any{4.8, 2.7, 6.8}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}},
|
||
"target": []any{0, 1, 0}, "focalLength": 35, "fov": 50, "aperture": 2.8, "focusDistance": 5, "near": 0.05, "far": 500, "keyframes": []any{},
|
||
}
|
||
objects := []any{}
|
||
lights := []any{
|
||
map[string]any{"id": args.SceneID + "-light-key", "name": "主光", "type": "directional", "transform": map[string]any{"position": []any{4, 6, 4}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#ffffff", "intensity": 2.4, "angle": 0.7853981634, "penumbra": 0.35, "castShadow": true},
|
||
map[string]any{"id": args.SceneID + "-light-fill", "name": "轮廓光", "type": "directional", "transform": map[string]any{"position": []any{-4, 3, -2}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#ffffff", "intensity": 1.1, "angle": 0.7853981634, "penumbra": 0.35, "castShadow": true},
|
||
map[string]any{"id": args.SceneID + "-light-ambient", "name": "环境光", "type": "ambient", "transform": map[string]any{"position": []any{0, 0, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#ffffff", "intensity": 0.65, "angle": 0.7853981634, "penumbra": 0.35, "castShadow": false},
|
||
}
|
||
if args.TemplateID == "action_chase" {
|
||
// Two actors: pursuer behind, target ahead, side-angle camera for kinetic energy
|
||
objects = append(objects,
|
||
map[string]any{"id": args.SceneID + "-actor-1", "name": "追逐者", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{0, 0, -1.2}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#2f7de1", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "run", "archetype": "man", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}},
|
||
map[string]any{"id": args.SceneID + "-actor-2", "name": "逃跑者", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{0, 0, 2.0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#d84949", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "run", "archetype": "adult", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}},
|
||
)
|
||
camera["transform"] = map[string]any{"position": []any{3.8, 1.4, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}
|
||
camera["target"] = []any{0, 1, 0.5}
|
||
camera["focalLength"] = 50
|
||
camera["fov"] = 39.6
|
||
}
|
||
if args.TemplateID == "interior" {
|
||
// Room sketch: floor plane, 3 wall planes, sofa-scale box, table-scale box
|
||
objects = append(objects,
|
||
map[string]any{"id": args.SceneID + "-wall-back", "name": "后墙", "kind": "primitive", "primitive": "plane", "transform": map[string]any{"position": []any{0, 2.0, -3.0}, "rotation": []any{0, 0, 0}, "scale": []any{6, 4, 1}}, "color": "#d8dde3", "visible": true, "castShadow": false, "receiveShadow": true, "keyframes": []any{}},
|
||
map[string]any{"id": args.SceneID + "-wall-left", "name": "左墙", "kind": "primitive", "primitive": "plane", "transform": map[string]any{"position": []any{-3.0, 2.0, 0}, "rotation": []any{0, 1.5708, 0}, "scale": []any{6, 4, 1}}, "color": "#d8dde3", "visible": true, "castShadow": false, "receiveShadow": true, "keyframes": []any{}},
|
||
map[string]any{"id": args.SceneID + "-sofa", "name": "沙发", "kind": "primitive", "primitive": "box", "transform": map[string]any{"position": []any{0, 0.45, -1.8}, "rotation": []any{0, 0, 0}, "scale": []any{2.2, 0.9, 0.9}}, "color": "#8795a5", "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}},
|
||
map[string]any{"id": args.SceneID + "-table", "name": "茶几", "kind": "primitive", "primitive": "box", "transform": map[string]any{"position": []any{0, 0.23, 0.2}, "rotation": []any{0, 0, 0}, "scale": []any{1.1, 0.46, 0.65}}, "color": "#6b7580", "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}},
|
||
map[string]any{"id": args.SceneID + "-actor-1", "name": "演员 1", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{-0.6, 0, 0.6}, "rotation": []any{0, -0.4, 0}, "scale": []any{1, 1, 1}}, "color": "#2f7de1", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "sit", "archetype": "adult", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}},
|
||
)
|
||
camera["transform"] = map[string]any{"position": []any{3.2, 1.65, 3.0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}
|
||
camera["target"] = []any{-0.2, 1.0, 0}
|
||
camera["focalLength"] = 35
|
||
}
|
||
if args.TemplateID == "crowd" {
|
||
// 3x3 crowd grid with distinct colors
|
||
crowdColors := []string{"#2f7de1", "#d84949", "#dfae3f", "#34a276", "#8b5cf6", "#e879f9", "#2f7de1", "#d84949", "#dfae3f"}
|
||
for row := 0; row < 3; row++ {
|
||
for col := 0; col < 3; col++ {
|
||
idx := row*3 + col
|
||
id := fmt.Sprintf("%s-actor-%d", args.SceneID, idx+1)
|
||
x := float64(col-1) * 1.2
|
||
z := float64(row-1) * 1.2
|
||
objects = append(objects, map[string]any{
|
||
"id": id, "name": fmt.Sprintf("群众 %d", idx+1), "kind": "actor", "primitive": "character",
|
||
"transform": map[string]any{"position": []any{x, 0, z}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}},
|
||
"color": crowdColors[idx], "visible": true, "castShadow": true, "receiveShadow": true,
|
||
"pose": "stand", "archetype": "adult", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{},
|
||
})
|
||
}
|
||
}
|
||
camera["transform"] = map[string]any{"position": []any{0.5, 4.5, 6.5}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}
|
||
camera["target"] = []any{0, 0.8, 0}
|
||
camera["focalLength"] = 28
|
||
camera["fov"] = 65.0
|
||
}
|
||
if args.TemplateID == "monologue" || args.TemplateID == "dialogue" || args.TemplateID == "blocking" {
|
||
objects = append(objects, map[string]any{"id": args.SceneID + "-actor-1", "name": "演员 1", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{0, 0, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}, "color": "#f1f3f5", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "stand", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}})
|
||
}
|
||
if args.TemplateID == "dialogue" || args.TemplateID == "blocking" {
|
||
objects = append(objects, map[string]any{"id": args.SceneID + "-actor-2", "name": "演员 2", "kind": "actor", "primitive": "character", "transform": map[string]any{"position": []any{1.8, 0, -0.8}, "rotation": []any{0, 0.2, 0}, "scale": []any{1, 1, 1}}, "color": "#2f7de1", "visible": true, "castShadow": true, "receiveShadow": true, "pose": "stand", "keyframes": []any{}, "boneTracks": []any{}, "boneOverrides": map[string]any{}, "motionClips": []any{}})
|
||
}
|
||
if args.TemplateID == "product" {
|
||
objects = append(objects, map[string]any{"id": args.SceneID + "-product-1", "name": "产品", "kind": "primitive", "primitive": "box", "transform": map[string]any{"position": []any{0, 0.6, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1.2, 1.2, 1.2}}, "color": "#8795a5", "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}})
|
||
}
|
||
scene := map[string]any{
|
||
"id": args.SceneID, "version": 1, "title": args.Title, "background": "#d8dde3", "environmentIntensity": 0.7, "gridVisible": true,
|
||
"objects": objects, "cameras": []any{camera}, "lights": lights,
|
||
"shots": []any{map[string]any{"id": shotID, "name": "镜头 1", "cameraId": cameraID, "duration": 5, "fps": 24, "shotSize": "medium", "cameraMove": "static", "prompt": "", "cuts": []any{}}},
|
||
"activeShotId": shotID, "createdAt": now, "updatedAt": now,
|
||
}
|
||
return scene, cloudAgentPrevisValidateScene(scene)
|
||
}
|
||
|
||
func cloudAgentPrevisWorkstationNodeID(sceneID string) string {
|
||
candidate := "previs-" + sceneID
|
||
if utf8.RuneCountInString(candidate) <= 80 {
|
||
return candidate
|
||
}
|
||
return "previs-" + creationHash(map[string]any{"sceneId": sceneID})[:40]
|
||
}
|
||
|
||
func cloudAgentPrevisActiveShot(scene map[string]any) (string, string, int, error) {
|
||
activeShotID := stringValue(scene["activeShotId"])
|
||
for index, shot := range creationMaps(scene["shots"]) {
|
||
if activeShotID != "" && stringValue(shot["id"]) != activeShotID {
|
||
continue
|
||
}
|
||
shotID := stringValue(shot["id"])
|
||
if shotID == "" {
|
||
return "", "", 0, BadAuthRequest("预演场景的活动镜头缺少稳定 ID")
|
||
}
|
||
shotTitle := stringValue(shot["name"])
|
||
if shotTitle == "" {
|
||
shotTitle = fmt.Sprintf("镜头 %d", index+1)
|
||
}
|
||
return shotID, shotTitle, index + 1, nil
|
||
}
|
||
return "", "", 0, BadAuthRequest("预演场景没有可绑定的活动镜头")
|
||
}
|
||
|
||
func cloudAgentPrevisWorkstationPosition(nodes []map[string]any) (float64, float64) {
|
||
right := math.Inf(-1)
|
||
y := 80.0
|
||
for _, node := range nodes {
|
||
position, _ := node["position"].(map[string]any)
|
||
x := numberValue(position["x"], 0)
|
||
width := numberValue(node["width"], 340)
|
||
if x+width > right {
|
||
right = x + width
|
||
y = math.Max(80, numberValue(position["y"], 80))
|
||
}
|
||
}
|
||
if math.IsInf(right, -1) {
|
||
return 80, 80
|
||
}
|
||
return math.Max(80, right+80), y
|
||
}
|
||
|
||
func cloudAgentPrevisEnsureWorkstationNode(doc map[string]any, scene map[string]any, requestedNodeID, requestedTitle string) (string, string, bool, error) {
|
||
sceneID := stringValue(scene["id"])
|
||
if err := validateCloudAgentID(sceneID, "导演场景 ID", 80); err != nil {
|
||
return "", "", false, err
|
||
}
|
||
shotID, shotTitle, shotIndex, err := cloudAgentPrevisActiveShot(scene)
|
||
if err != nil {
|
||
return "", "", false, err
|
||
}
|
||
|
||
nodeID := strings.TrimSpace(requestedNodeID)
|
||
if nodeID != "" {
|
||
if err := validateCloudAgentID(nodeID, "预演工作站节点 ID", 80); err != nil {
|
||
return "", "", false, err
|
||
}
|
||
} else {
|
||
nodeID = cloudAgentPrevisWorkstationNodeID(sceneID)
|
||
}
|
||
title := strings.TrimSpace(requestedTitle)
|
||
if title == "" {
|
||
title = stringValue(scene["title"])
|
||
}
|
||
if title == "" {
|
||
title = shotTitle
|
||
}
|
||
if err := cloudAgentPrevisValidateText(title, "预演工作站标题", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return "", "", false, err
|
||
}
|
||
|
||
nodes := creationMaps(doc["nodes"])
|
||
var bound map[string]any
|
||
for _, node := range nodes {
|
||
metadata, _ := node["metadata"].(map[string]any)
|
||
if stringValue(metadata["previsSceneId"]) != sceneID {
|
||
continue
|
||
}
|
||
if stringValue(node["type"]) != "video" {
|
||
return "", "", false, BadAuthRequest("预演场景已绑定非视频节点,无法修复工作站")
|
||
}
|
||
if bound != nil && stringValue(bound["id"]) != stringValue(node["id"]) {
|
||
return "", "", false, BadAuthRequest("预演场景绑定了多个工作站节点,无法自动选择")
|
||
}
|
||
bound = node
|
||
}
|
||
|
||
var requested map[string]any
|
||
for _, node := range nodes {
|
||
if stringValue(node["id"]) == nodeID {
|
||
requested = node
|
||
break
|
||
}
|
||
}
|
||
if requested != nil {
|
||
if stringValue(requested["type"]) != "video" {
|
||
return "", "", false, BadAuthRequest("预演工作站节点 ID 已被非视频节点占用")
|
||
}
|
||
metadata, _ := requested["metadata"].(map[string]any)
|
||
otherSceneID := stringValue(metadata["previsSceneId"])
|
||
if otherSceneID != "" && otherSceneID != sceneID {
|
||
return "", "", false, BadAuthRequest("预演工作站节点 ID 已绑定其他场景")
|
||
}
|
||
if bound != nil && stringValue(bound["id"]) != nodeID {
|
||
return "", "", false, BadAuthRequest("预演场景已有其他工作站节点绑定")
|
||
}
|
||
bound = requested
|
||
}
|
||
|
||
created := false
|
||
if bound == nil {
|
||
positionX, positionY := cloudAgentPrevisWorkstationPosition(nodes)
|
||
metadata := map[string]any{
|
||
"content": "",
|
||
"status": "idle",
|
||
"workflowKind": "shot",
|
||
"workflowTitle": shotTitle,
|
||
"shotIndex": float64(shotIndex),
|
||
"generationMode": "video",
|
||
"videoEditOperation": "text_to_video",
|
||
"composerContent": "",
|
||
"previsSceneId": sceneID,
|
||
"previsShotId": shotID,
|
||
}
|
||
width, height := cloudAgentPrevisWorkstationWidth, cloudAgentPrevisWorkstationHeight
|
||
op := CreationCanvasOp{Type: "add_node", ID: nodeID, NodeType: "video", Title: title, X: &positionX, Y: &positionY, Width: &width, Height: &height, Metadata: metadata}
|
||
nodes = append(nodes, creationAddedNode(op))
|
||
doc["nodes"] = nodes
|
||
return nodeID, shotID, true, nil
|
||
}
|
||
|
||
metadata, _ := bound["metadata"].(map[string]any)
|
||
if metadata == nil {
|
||
metadata = map[string]any{}
|
||
}
|
||
metadata["workflowKind"] = "shot"
|
||
metadata["workflowTitle"] = shotTitle
|
||
metadata["shotIndex"] = float64(shotIndex)
|
||
metadata["generationMode"] = "video"
|
||
metadata["videoEditOperation"] = "text_to_video"
|
||
metadata["previsSceneId"] = sceneID
|
||
metadata["previsShotId"] = shotID
|
||
if _, exists := metadata["status"]; !exists {
|
||
metadata["status"] = "idle"
|
||
}
|
||
if _, exists := metadata["composerContent"]; !exists {
|
||
metadata["composerContent"] = ""
|
||
}
|
||
bound["metadata"] = metadata
|
||
if stringValue(bound["title"]) == "" || strings.TrimSpace(requestedTitle) != "" {
|
||
bound["title"] = title
|
||
}
|
||
doc["nodes"] = nodes
|
||
return stringValue(bound["id"]), shotID, created, nil
|
||
}
|
||
|
||
func prepareCloudAgentPrevisSceneCreate(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (*cloudAgentPrevisMutationPlan, error) {
|
||
var args cloudAgentPrevisSceneCreateArgs
|
||
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
|
||
return nil, cloudAgentJSONArgumentError(err)
|
||
}
|
||
if strings.TrimSpace(args.CanvasSnapshotHash) == "" {
|
||
return nil, cloudAgentFieldError("canvasSnapshotHash", "required", "创建预演场景前必须先读取场景目录")
|
||
}
|
||
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
|
||
return nil, cloudAgentFieldError("sceneId", "invalid_value", cloudAgentSafeToolError(err))
|
||
}
|
||
if args.NodeID != "" {
|
||
if err := validateCloudAgentID(args.NodeID, "预演工作站节点 ID", 80); err != nil {
|
||
return nil, cloudAgentFieldError("nodeId", "invalid_value", cloudAgentSafeToolError(err))
|
||
}
|
||
}
|
||
if args.NodeTitle != "" {
|
||
if err := cloudAgentPrevisValidateText(args.NodeTitle, "预演工作站标题", cloudAgentPrevisTextLimit, false); err != nil {
|
||
return nil, cloudAgentFieldError("nodeTitle", "invalid_value", cloudAgentSafeToolError(err))
|
||
}
|
||
}
|
||
canvas, doc, err := cloudAgentPrevisCanvas(repo, userID, canvasID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
beforeHash := cloudAgentCanvasHash(doc)
|
||
if beforeHash != args.CanvasSnapshotHash {
|
||
return nil, &cloudAgentFieldArgumentError{error: &cloudAgentArgumentError{creationConflict("画布已变化,本次未创建场景;请重新读取场景目录")}, Field: "canvasSnapshotHash", Issue: "stale_snapshot"}
|
||
}
|
||
scenes := creationMaps(doc["previsScenes"])
|
||
if len(scenes) >= cloudAgentPrevisMaxScenes {
|
||
return nil, BadAuthRequest("当前画布的预演场景数量已达到限制")
|
||
}
|
||
if _, exists := findPrevisScene(scenes, args.SceneID); exists {
|
||
return nil, BadAuthRequest("预演场景 ID 已存在")
|
||
}
|
||
scene, err := cloudAgentPrevisSceneCreateTemplate(args)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scenes = append(scenes, scene)
|
||
if err := cloudAgentPrevisScenesSize(scenes); err != nil {
|
||
return nil, err
|
||
}
|
||
doc["previsScenes"] = scenes
|
||
nodeID, shotID, _, err := cloudAgentPrevisEnsureWorkstationNode(doc, scene, args.NodeID, args.NodeTitle)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
details := []string{"创建受控白模场景", "模板:" + args.TemplateID, "绑定预演视频工作站:" + nodeID}
|
||
return &cloudAgentPrevisMutationPlan{Canvas: canvas, Document: doc, BeforeJSON: canvas.PayloadJSON, BeforeSnapshotHash: beforeHash, AfterSnapshotHash: cloudAgentCanvasHash(doc), ResultSnapshotHash: creationHash(scene), Operation: "previs_scene_create", SceneID: args.SceneID, NodeID: nodeID, ShotID: shotID, Preview: cloudAgentPrevisMutationApprovalPreview("previs_scene_create", args.SceneID, args.Title, details)}, nil
|
||
}
|
||
|
||
func prepareCloudAgentPrevisApplyPatch(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (*cloudAgentPrevisMutationPlan, error) {
|
||
var args cloudAgentPrevisApplyPatchArgs
|
||
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
|
||
return nil, cloudAgentJSONArgumentError(err)
|
||
}
|
||
if strings.TrimSpace(args.SnapshotHash) == "" {
|
||
return nil, cloudAgentFieldError("snapshotHash", "required", "应用预演补丁前必须先读取指定场景")
|
||
}
|
||
if len(args.Operations) < 1 || len(args.Operations) > cloudAgentPrevisMaxOperations {
|
||
return nil, cloudAgentFieldError("operations", "item_count", fmt.Sprintf("预演补丁 operations 必须包含1到%d项操作", cloudAgentPrevisMaxOperations))
|
||
}
|
||
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
|
||
return nil, err
|
||
}
|
||
canvas, doc, err := cloudAgentPrevisCanvas(repo, userID, canvasID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scenes := creationMaps(doc["previsScenes"])
|
||
scene, ok := findPrevisScene(scenes, args.SceneID)
|
||
if !ok {
|
||
return nil, BadAuthRequest("导演场景不存在或不属于当前画布")
|
||
}
|
||
canvasSnapshotHash := cloudAgentCanvasHash(doc)
|
||
sceneSnapshotHash := creationHash(scene)
|
||
if sceneSnapshotHash != args.SnapshotHash {
|
||
return nil, &cloudAgentFieldArgumentError{error: &cloudAgentArgumentError{creationConflict("预演场景已变化,本次未写入;请重新读取并重新申请审批")}, Field: "snapshotHash", Issue: "stale_snapshot"}
|
||
}
|
||
items := make([]cloudAgentApprovalPreviewItem, 0, len(args.Operations))
|
||
for index, operation := range args.Operations {
|
||
if err := cloudAgentPrevisValidateCharacterBindingInCanvas(doc, operation); err != nil {
|
||
return nil, err
|
||
}
|
||
item, err := cloudAgentPrevisApplyPatchOperation(scene, operation, index)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
scene["updatedAt"] = time.Now().UTC().Format(time.RFC3339Nano)
|
||
if err := cloudAgentPrevisValidateScene(scene); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cloudAgentPrevisScenesSize(scenes); err != nil {
|
||
return nil, err
|
||
}
|
||
doc["previsScenes"] = scenes
|
||
nodeID, shotID, workstationCreated, err := cloudAgentPrevisEnsureWorkstationNode(doc, scene, "", "")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
details := itemsToDetails(items)
|
||
if workstationCreated {
|
||
details = append(details, "创建并绑定预演视频工作站:"+nodeID)
|
||
}
|
||
return &cloudAgentPrevisMutationPlan{Canvas: canvas, Document: doc, BeforeJSON: canvas.PayloadJSON, BeforeSnapshotHash: canvasSnapshotHash, AfterSnapshotHash: cloudAgentCanvasHash(doc), ResultSnapshotHash: creationHash(scene), Operation: "previs_apply_patch", SceneID: args.SceneID, NodeID: nodeID, ShotID: shotID, Preview: cloudAgentPrevisMutationApprovalPreview("previs_apply_patch", args.SceneID, stringValue(scene["title"]), details)}, nil
|
||
}
|
||
|
||
func itemsToDetails(items []cloudAgentApprovalPreviewItem) []string {
|
||
result := make([]string, 0, len(items))
|
||
for _, item := range items {
|
||
if item.Summary != "" {
|
||
result = append(result, item.Summary)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
func cloudAgentPrevisMutationApprovalPreview(operation, sceneID, title string, details []string) cloudAgentApprovalPreview {
|
||
if len(details) > 12 {
|
||
details = details[:12]
|
||
}
|
||
return cloudAgentApprovalPreview{Kind: "previs_mutation", Title: "确认预演台修改", Description: fmt.Sprintf("Agent 准备修改预演场景《%s》。批准后才会写入当前画布。", truncateRunes(title, cloudAgentPrevisTextLimit)), Items: []cloudAgentApprovalPreviewItem{{Operation: operation, NodeID: sceneID, NodeTitle: truncateRunes(title, cloudAgentPrevisTextLimit), Details: details, Summary: fmt.Sprintf("修改预演场景《%s》", truncateRunes(title, cloudAgentPrevisTextLimit))}}}
|
||
}
|
||
|
||
func cloudAgentPrevisMapTransform(item map[string]any) map[string]any {
|
||
if transform, ok := item["transform"].(map[string]any); ok {
|
||
return transform
|
||
}
|
||
transform := map[string]any{"position": []any{0, 0, 0}, "rotation": []any{0, 0, 0}, "scale": []any{1, 1, 1}}
|
||
item["transform"] = transform
|
||
return transform
|
||
}
|
||
|
||
func cloudAgentPrevisApplyTransform(item map[string]any, operation cloudAgentPrevisPatchOperation) error {
|
||
if err := cloudAgentPrevisOptionalVec3(operation.Position, "position"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisOptionalVec3(operation.Rotation, "rotation"); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisOptionalVec3(operation.Scale, "scale"); err != nil {
|
||
return err
|
||
}
|
||
transform := cloudAgentPrevisMapTransform(item)
|
||
if operation.Position != nil {
|
||
transform["position"] = cloudAgentPrevisVec3(*operation.Position)
|
||
}
|
||
if operation.Rotation != nil {
|
||
transform["rotation"] = cloudAgentPrevisVec3(*operation.Rotation)
|
||
}
|
||
if operation.Scale != nil {
|
||
transform["scale"] = cloudAgentPrevisVec3(*operation.Scale)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisApplyName(item map[string]any, value *string, label string) error {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
if err := cloudAgentPrevisValidateText(*value, label, cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
item["name"] = *value
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisApplyCharacterBinding(item map[string]any, operation cloudAgentPrevisPatchOperation) error {
|
||
// characterName is descriptive metadata for a real character-card binding;
|
||
// a name on an actor from an ordinary reference image must not trigger binding.
|
||
provided := operation.CharacterAssetID != nil || operation.CharacterVersionID != nil || operation.ReferenceNodeID != nil
|
||
if !provided {
|
||
return nil
|
||
}
|
||
if operation.CharacterAssetID == nil || operation.CharacterVersionID == nil || operation.ReferenceNodeID == nil {
|
||
return BadAuthRequest("角色绑定需要 characterAssetId、characterVersionId 和 referenceNodeId")
|
||
}
|
||
for value, label := range map[*string]string{
|
||
operation.CharacterAssetID: "characterAssetId",
|
||
operation.CharacterVersionID: "characterVersionId",
|
||
operation.ReferenceNodeID: "referenceNodeId",
|
||
} {
|
||
if value == nil || strings.TrimSpace(*value) == "" {
|
||
return BadAuthRequest(label + " 不能为空")
|
||
}
|
||
if err := validateCloudAgentID(*value, label, 120); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
binding := map[string]any{
|
||
"characterAssetId": *operation.CharacterAssetID,
|
||
"characterVersionId": *operation.CharacterVersionID,
|
||
"referenceNodeId": *operation.ReferenceNodeID,
|
||
}
|
||
if operation.CharacterName != nil {
|
||
if err := cloudAgentPrevisValidateText(*operation.CharacterName, "characterName", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return err
|
||
}
|
||
binding["characterName"] = *operation.CharacterName
|
||
}
|
||
item["characterBinding"] = binding
|
||
item["sourceNodeId"] = *operation.ReferenceNodeID
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisValidateCharacterBindingInCanvas(doc map[string]any, operation cloudAgentPrevisPatchOperation) error {
|
||
if operation.CharacterAssetID == nil && operation.CharacterVersionID == nil && operation.ReferenceNodeID == nil {
|
||
return nil
|
||
}
|
||
if operation.CharacterAssetID == nil || operation.CharacterVersionID == nil || operation.ReferenceNodeID == nil {
|
||
return BadAuthRequest("角色绑定需要 characterAssetId、characterVersionId 和 referenceNodeId")
|
||
}
|
||
for _, node := range creationMaps(doc["nodes"]) {
|
||
if stringValue(node["id"]) != *operation.ReferenceNodeID {
|
||
continue
|
||
}
|
||
metadata, _ := node["metadata"].(map[string]any)
|
||
if stringValue(metadata["workflowKind"]) != "character" || stringValue(metadata["characterAssetId"]) != *operation.CharacterAssetID || stringValue(metadata["characterVersionId"]) != *operation.CharacterVersionID {
|
||
return BadAuthRequest("referenceNodeId 必须是匹配 characterAssetId 和 characterVersionId 的角色卡节点")
|
||
}
|
||
return nil
|
||
}
|
||
return BadAuthRequest("referenceNodeId 不属于当前画布")
|
||
}
|
||
|
||
func cloudAgentPrevisShotDuration(scene map[string]any) float64 {
|
||
for _, shot := range creationMaps(scene["shots"]) {
|
||
if stringValue(shot["id"]) == stringValue(scene["activeShotId"]) {
|
||
return numberValue(shot["duration"], 5)
|
||
}
|
||
}
|
||
return 5
|
||
}
|
||
|
||
func cloudAgentPrevisKeyframeTransform(item map[string]any, operation cloudAgentPrevisPatchOperation) (map[string]any, error) {
|
||
transform := cloudAgentPrevisMapTransform(item)
|
||
result := map[string]any{}
|
||
for _, key := range []string{"position", "rotation", "scale"} {
|
||
value, ok := cloudAgentPrevisNumberVec3(transform[key])
|
||
if !ok {
|
||
return nil, BadAuthRequest("关键帧目标对象 transform 无效")
|
||
}
|
||
result[key] = cloudAgentPrevisVec3(value)
|
||
}
|
||
for value, key := range map[*[]float64]string{operation.Position: "position", operation.Rotation: "rotation", operation.Scale: "scale"} {
|
||
if value == nil {
|
||
continue
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(*value, key); err != nil {
|
||
return nil, err
|
||
}
|
||
result[key] = cloudAgentPrevisVec3(*value)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func cloudAgentPrevisValidateEasing(value string) error {
|
||
if value == "" {
|
||
return nil
|
||
}
|
||
return cloudAgentPrevisValidateEnum(value, "关键帧 easing", "step", "linear", "smooth")
|
||
}
|
||
|
||
func cloudAgentPrevisUpsertKeyframe(item map[string]any, operation cloudAgentPrevisPatchOperation, duration float64) error {
|
||
if operation.Time == nil {
|
||
return BadAuthRequest("关键帧操作需要 time")
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.Time, "关键帧 time", 0, duration); err != nil {
|
||
return err
|
||
}
|
||
if err := cloudAgentPrevisValidateEasing(operation.Easing); err != nil {
|
||
return err
|
||
}
|
||
transform, err := cloudAgentPrevisKeyframeTransform(item, operation)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
keyframes := creationMaps(item["keyframes"])
|
||
keyframeID := ""
|
||
if operation.KeyframeID != nil {
|
||
if err := validateCloudAgentID(*operation.KeyframeID, "keyframeId", 120); err != nil {
|
||
return err
|
||
}
|
||
keyframeID = *operation.KeyframeID
|
||
}
|
||
match := -1
|
||
for index, keyframe := range keyframes {
|
||
if (keyframeID != "" && stringValue(keyframe["id"]) == keyframeID) || math.Abs(numberValue(keyframe["time"], math.NaN())-*operation.Time) < 0.001 {
|
||
match = index
|
||
break
|
||
}
|
||
}
|
||
if match < 0 {
|
||
if keyframeID == "" {
|
||
keyframeID = fmt.Sprintf("%s-kf-%d", stringValue(item["id"]), len(keyframes)+1)
|
||
}
|
||
keyframe := map[string]any{"id": keyframeID, "time": *operation.Time, "transform": transform}
|
||
if operation.Easing != "" {
|
||
keyframe["easing"] = operation.Easing
|
||
}
|
||
keyframes = append(keyframes, keyframe)
|
||
} else {
|
||
keyframes[match]["id"] = keyframeIDOrExisting(keyframeID, keyframes[match])
|
||
keyframes[match]["time"] = *operation.Time
|
||
keyframes[match]["transform"] = transform
|
||
if operation.Easing == "" {
|
||
delete(keyframes[match], "easing")
|
||
} else {
|
||
keyframes[match]["easing"] = operation.Easing
|
||
}
|
||
}
|
||
for i := range keyframes {
|
||
for j := i + 1; j < len(keyframes); j++ {
|
||
if numberValue(keyframes[j]["time"], 0) < numberValue(keyframes[i]["time"], 0) {
|
||
keyframes[i], keyframes[j] = keyframes[j], keyframes[i]
|
||
}
|
||
}
|
||
}
|
||
item["keyframes"] = keyframes
|
||
return nil
|
||
}
|
||
|
||
func keyframeIDOrExisting(requested string, existing map[string]any) string {
|
||
if requested != "" {
|
||
return requested
|
||
}
|
||
return stringValue(existing["id"])
|
||
}
|
||
|
||
func cloudAgentPrevisRemoveKeyframe(item map[string]any, operation cloudAgentPrevisPatchOperation) error {
|
||
if operation.KeyframeID == nil || strings.TrimSpace(*operation.KeyframeID) == "" {
|
||
return BadAuthRequest("删除关键帧需要 keyframeId")
|
||
}
|
||
if err := validateCloudAgentID(*operation.KeyframeID, "keyframeId", 120); err != nil {
|
||
return err
|
||
}
|
||
keyframes := creationMaps(item["keyframes"])
|
||
filtered := make([]map[string]any, 0, len(keyframes))
|
||
for _, keyframe := range keyframes {
|
||
if stringValue(keyframe["id"]) != *operation.KeyframeID {
|
||
filtered = append(filtered, keyframe)
|
||
}
|
||
}
|
||
if len(filtered) == len(keyframes) {
|
||
return BadAuthRequest("目标关键帧不存在")
|
||
}
|
||
item["keyframes"] = filtered
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisSetMotionPath(item map[string]any, operation cloudAgentPrevisPatchOperation, duration float64) error {
|
||
if len(operation.PathPoints) < 2 || len(operation.PathPoints) > 64 {
|
||
return BadAuthRequest("motion path 至少需要2个、最多64个点")
|
||
}
|
||
for index, point := range operation.PathPoints {
|
||
if err := cloudAgentPrevisValidateVec3(point, fmt.Sprintf("pathPoints[%d]", index)); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
speed := operation.PathSpeed
|
||
if speed == "" {
|
||
speed = "walk"
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(speed, "pathSpeed", "slow", "walk", "run"); err != nil {
|
||
return err
|
||
}
|
||
startDelay := 0.0
|
||
if operation.StartDelay != nil {
|
||
startDelay = *operation.StartDelay
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(startDelay, "startDelay", 0, duration); err != nil {
|
||
return err
|
||
}
|
||
orient := false
|
||
if operation.OrientToPath != nil {
|
||
orient = *operation.OrientToPath
|
||
}
|
||
points := make([]any, 0, len(operation.PathPoints))
|
||
for _, point := range operation.PathPoints {
|
||
points = append(points, cloudAgentPrevisVec3(point))
|
||
}
|
||
item["motionPath"] = map[string]any{"points": points, "speed": speed, "startDelay": startDelay, "orientToPath": orient}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisApplyObjectPatch(objects *[]map[string]any, operation cloudAgentPrevisPatchOperation, index int) (cloudAgentApprovalPreviewItem, error) {
|
||
items := *objects
|
||
byID, err := cloudAgentPrevisIDSet(items, "导演对象 ID")
|
||
if err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := validateCloudAgentID(operation.ID, fmt.Sprintf("operations[%d].id", index), 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
existing := byID[operation.ID]
|
||
summary := ""
|
||
switch operation.Type {
|
||
case "object_add":
|
||
if existing != nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增对象 ID 已存在")
|
||
}
|
||
if operation.Kind != "primitive" && operation.Kind != "actor" {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增对象 kind 只支持 primitive 或 actor")
|
||
}
|
||
primitive := operation.Primitive
|
||
if primitive == "" {
|
||
primitive = "box"
|
||
}
|
||
if primitive != "box" && primitive != "sphere" && primitive != "cylinder" && primitive != "plane" && primitive != "character" {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增对象 primitive 不受支持")
|
||
}
|
||
name := "新对象"
|
||
if operation.Name != nil {
|
||
name = *operation.Name
|
||
}
|
||
if err := cloudAgentPrevisValidateText(name, "对象名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
position, rotation, scale := []float64{0, 0.5, 0}, []float64{0, 0, 0}, []float64{1, 1, 1}
|
||
if operation.Position != nil {
|
||
position = *operation.Position
|
||
}
|
||
if operation.Rotation != nil {
|
||
rotation = *operation.Rotation
|
||
}
|
||
if operation.Scale != nil {
|
||
scale = *operation.Scale
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(position, "position"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(rotation, "rotation"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(scale, "scale"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if operation.Color != nil {
|
||
if err := cloudAgentPrevisValidateColor(*operation.Color, "对象颜色"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
}
|
||
if operation.Pose != "" {
|
||
if err := cloudAgentPrevisValidatePose(operation.Pose); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
}
|
||
if operation.ParentID != nil && *operation.ParentID != "" {
|
||
if err := validateCloudAgentID(*operation.ParentID, "对象 parentId", 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
}
|
||
defaultColor := "#8795a5"
|
||
if operation.Kind == "actor" {
|
||
// Assign a distinct colour based on current actor count so each new actor stands out
|
||
actorCount := 0
|
||
for _, obj := range items {
|
||
if stringValue(obj["kind"]) == "actor" {
|
||
actorCount++
|
||
}
|
||
}
|
||
actorColors := []string{"#2f7de1", "#d84949", "#dfae3f", "#34a276", "#8b5cf6", "#e879f9"}
|
||
defaultColor = actorColors[actorCount%len(actorColors)]
|
||
}
|
||
item := map[string]any{"id": operation.ID, "name": name, "kind": operation.Kind, "primitive": primitive, "transform": map[string]any{"position": cloudAgentPrevisVec3(position), "rotation": cloudAgentPrevisVec3(rotation), "scale": cloudAgentPrevisVec3(scale)}, "color": defaultColor, "visible": true, "castShadow": true, "receiveShadow": true, "keyframes": []any{}}
|
||
if operation.Color != nil {
|
||
item["color"] = *operation.Color
|
||
}
|
||
if operation.Visible != nil {
|
||
item["visible"] = *operation.Visible
|
||
}
|
||
if operation.Pose != "" {
|
||
item["pose"] = operation.Pose
|
||
}
|
||
if operation.Kind == "actor" && operation.Pose == "" {
|
||
item["pose"] = "stand"
|
||
}
|
||
if operation.Kind == "actor" {
|
||
item["archetype"] = "adult"
|
||
item["boneTracks"] = []any{}
|
||
item["boneOverrides"] = map[string]any{}
|
||
item["motionClips"] = []any{}
|
||
}
|
||
if operation.Archetype != "" {
|
||
validArchetypes := map[string]bool{"adult": true, "child": true, "elderly": true, "man": true, "woman": true, "monster": true}
|
||
if !validArchetypes[operation.Archetype] {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("archetype 值不受支持")
|
||
}
|
||
item["archetype"] = operation.Archetype
|
||
}
|
||
if operation.ParentID != nil && *operation.ParentID != "" {
|
||
item["parentId"] = *operation.ParentID
|
||
}
|
||
if err := cloudAgentPrevisApplyCharacterBinding(item, operation); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
items = append(items, item)
|
||
summary = fmt.Sprintf("新增对象《%s》", name)
|
||
case "object_update":
|
||
if existing == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("要更新的对象不存在")
|
||
}
|
||
if err := cloudAgentPrevisApplyName(existing, operation.Name, "对象名称"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if operation.Color != nil {
|
||
if err := cloudAgentPrevisValidateColor(*operation.Color, "对象颜色"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
existing["color"] = *operation.Color
|
||
}
|
||
if operation.Visible != nil {
|
||
existing["visible"] = *operation.Visible
|
||
}
|
||
if operation.Pose != "" {
|
||
if err := cloudAgentPrevisValidatePose(operation.Pose); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
existing["pose"] = operation.Pose
|
||
}
|
||
if operation.ParentID != nil {
|
||
if *operation.ParentID == "" {
|
||
delete(existing, "parentId")
|
||
} else {
|
||
if err := validateCloudAgentID(*operation.ParentID, "对象 parentId", 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
existing["parentId"] = *operation.ParentID
|
||
}
|
||
}
|
||
if err := cloudAgentPrevisApplyCharacterBinding(existing, operation); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisApplyTransform(existing, operation); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
summary = fmt.Sprintf("修改对象《%s》", stringValue(existing["name"]))
|
||
case "object_remove":
|
||
if existing == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("要删除的对象不存在")
|
||
}
|
||
for _, candidate := range items {
|
||
if stringValue(candidate["parentId"]) == operation.ID {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("存在子对象时不能删除父对象")
|
||
}
|
||
}
|
||
for i, candidate := range items {
|
||
if stringValue(candidate["id"]) == operation.ID {
|
||
items = append(items[:i], items[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
summary = fmt.Sprintf("删除对象《%s》", stringValue(existing["name"]))
|
||
default:
|
||
return cloudAgentApprovalPreviewItem{}, nil
|
||
}
|
||
*objects = items
|
||
nodeTitle := stringValue(existing["name"])
|
||
if nodeTitle == "" && operation.Type == "object_add" {
|
||
nodeTitle = "新对象"
|
||
if operation.Name != nil {
|
||
nodeTitle = *operation.Name
|
||
}
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: truncateRunes(nodeTitle, cloudAgentPrevisTextLimit), Summary: summary}, nil
|
||
}
|
||
|
||
func cloudAgentPrevisFindByID(items []map[string]any, id string) map[string]any {
|
||
for _, item := range items {
|
||
if stringValue(item["id"]) == id {
|
||
return item
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func cloudAgentPrevisApplyPatchOperation(scene map[string]any, operation cloudAgentPrevisPatchOperation, index int) (cloudAgentApprovalPreviewItem, error) {
|
||
if err := cloudAgentPrevisValidateText(operation.Type, fmt.Sprintf("operations[%d].type", index), 40, true); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := validateCloudAgentID(operation.ID, fmt.Sprintf("operations[%d].id", index), 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if operation.Type == "scene_update" && operation.ID != stringValue(scene["id"]) {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("scene_update 的 id 必须是当前场景 ID")
|
||
}
|
||
objects, cameras, lights, shots := creationMaps(scene["objects"]), creationMaps(scene["cameras"]), creationMaps(scene["lights"]), creationMaps(scene["shots"])
|
||
switch operation.Type {
|
||
case "scene_update":
|
||
if operation.Title != nil {
|
||
if err := cloudAgentPrevisValidateText(*operation.Title, "场景标题", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
scene["title"] = *operation.Title
|
||
}
|
||
if operation.Background != nil {
|
||
if err := cloudAgentPrevisValidateColor(*operation.Background, "场景背景色"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
scene["background"] = *operation.Background
|
||
}
|
||
if operation.EnvironmentIntensity != nil {
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.EnvironmentIntensity, "environmentIntensity", 0, 100); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
scene["environmentIntensity"] = *operation.EnvironmentIntensity
|
||
}
|
||
if operation.GridVisible != nil {
|
||
scene["gridVisible"] = *operation.GridVisible
|
||
}
|
||
if operation.ActiveShotID != nil {
|
||
if *operation.ActiveShotID == "" {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("activeShotId 不能为空")
|
||
}
|
||
if err := validateCloudAgentID(*operation.ActiveShotID, "activeShotId", 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
scene["activeShotId"] = *operation.ActiveShotID
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: stringValue(scene["id"]), NodeTitle: stringValue(scene["title"]), Summary: "修改场景级设置"}, nil
|
||
case "object_add", "object_update", "object_remove":
|
||
item, err := cloudAgentPrevisApplyObjectPatch(&objects, operation, index)
|
||
if err == nil {
|
||
scene["objects"] = objects
|
||
}
|
||
return item, err
|
||
case "object_keyframe_upsert", "object_keyframe_remove", "object_motion_path_set", "object_motion_path_clear":
|
||
object := cloudAgentPrevisFindByID(objects, operation.ID)
|
||
if object == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标对象不存在")
|
||
}
|
||
duration := cloudAgentPrevisShotDuration(scene)
|
||
var err error
|
||
switch operation.Type {
|
||
case "object_keyframe_upsert":
|
||
err = cloudAgentPrevisUpsertKeyframe(object, operation, duration)
|
||
case "object_keyframe_remove":
|
||
err = cloudAgentPrevisRemoveKeyframe(object, operation)
|
||
case "object_motion_path_set":
|
||
err = cloudAgentPrevisSetMotionPath(object, operation, duration)
|
||
case "object_motion_path_clear":
|
||
delete(object, "motionPath")
|
||
}
|
||
if err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
scene["objects"] = objects
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(object["name"]), Summary: fmt.Sprintf("修改对象《%s》的动画", stringValue(object["name"]))}, nil
|
||
case "shot_add":
|
||
if cloudAgentPrevisFindByID(shots, operation.ID) != nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增镜头 ID 已存在")
|
||
}
|
||
cameraID := ""
|
||
if operation.CameraID != nil {
|
||
if err := validateCloudAgentID(*operation.CameraID, "摄影机 ID", 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
cameraID = *operation.CameraID
|
||
} else if len(cameras) > 0 {
|
||
cameraID = stringValue(cameras[0]["id"])
|
||
}
|
||
if cloudAgentPrevisFindByID(cameras, cameraID) == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增镜头必须引用现有摄影机")
|
||
}
|
||
name := "新镜头"
|
||
if operation.Name != nil {
|
||
name = *operation.Name
|
||
}
|
||
if err := cloudAgentPrevisValidateText(name, "镜头名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
duration, fps := 5.0, 24
|
||
if operation.Duration != nil {
|
||
duration = *operation.Duration
|
||
}
|
||
if operation.FPS != nil {
|
||
fps = *operation.FPS
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(duration, "镜头时长", 0.1, 60); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if fps < 1 || fps > 60 {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("镜头帧率超出限制")
|
||
}
|
||
shotSize, cameraMove := operation.ShotSize, operation.CameraMove
|
||
if shotSize == "" {
|
||
shotSize = "medium"
|
||
}
|
||
if cameraMove == "" {
|
||
cameraMove = "static"
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(shotSize, "镜头景别", "extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(cameraMove, "镜头运镜", "static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
shot := map[string]any{"id": operation.ID, "name": name, "cameraId": cameraID, "duration": duration, "fps": fps, "shotSize": shotSize, "cameraMove": cameraMove, "prompt": "", "cuts": []any{}}
|
||
if operation.Prompt != nil {
|
||
if err := cloudAgentPrevisValidateText(*operation.Prompt, "镜头提示词", 2000, false); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
shot["prompt"] = *operation.Prompt
|
||
}
|
||
scene["shots"] = append(shots, shot)
|
||
if stringValue(scene["activeShotId"]) == "" {
|
||
scene["activeShotId"] = operation.ID
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: name, Summary: fmt.Sprintf("新增镜头《%s》", name)}, nil
|
||
case "shot_update", "shot_remove":
|
||
shot := cloudAgentPrevisFindByID(shots, operation.ID)
|
||
if shot == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标镜头不存在")
|
||
}
|
||
if operation.Type == "shot_remove" {
|
||
if len(shots) <= 1 {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("预演场景至少保留一个镜头")
|
||
}
|
||
next := make([]map[string]any, 0, len(shots)-1)
|
||
for _, item := range shots {
|
||
if stringValue(item["id"]) != operation.ID {
|
||
next = append(next, item)
|
||
}
|
||
}
|
||
scene["shots"] = next
|
||
if stringValue(scene["activeShotId"]) == operation.ID {
|
||
scene["activeShotId"] = stringValue(next[0]["id"])
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(shot["name"]), Summary: fmt.Sprintf("删除镜头《%s》", stringValue(shot["name"]))}, nil
|
||
}
|
||
if err := cloudAgentPrevisApplyName(shot, operation.Name, "镜头名称"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if operation.CameraID != nil {
|
||
if err := validateCloudAgentID(*operation.CameraID, "摄影机 ID", 80); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if cloudAgentPrevisFindByID(cameras, *operation.CameraID) == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("镜头引用的摄影机不存在")
|
||
}
|
||
shot["cameraId"] = *operation.CameraID
|
||
}
|
||
if operation.Duration != nil {
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.Duration, "镜头时长", 0.1, 60); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
shot["duration"] = *operation.Duration
|
||
}
|
||
if operation.FPS != nil {
|
||
if *operation.FPS < 1 || *operation.FPS > 60 {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("镜头帧率超出限制")
|
||
}
|
||
shot["fps"] = *operation.FPS
|
||
}
|
||
if operation.ShotSize != "" {
|
||
if err := cloudAgentPrevisValidateEnum(operation.ShotSize, "镜头景别", "extreme_wide", "wide", "full", "medium", "close_up", "extreme_close_up"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
shot["shotSize"] = operation.ShotSize
|
||
}
|
||
if operation.CameraMove != "" {
|
||
if err := cloudAgentPrevisValidateEnum(operation.CameraMove, "镜头运镜", "static", "push_in", "pull_out", "pan_left", "pan_right", "tilt_up", "tilt_down", "orbit_left", "orbit_right", "handheld"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
shot["cameraMove"] = operation.CameraMove
|
||
}
|
||
if operation.Prompt != nil {
|
||
if err := cloudAgentPrevisValidateText(*operation.Prompt, "镜头提示词", 2000, false); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
shot["prompt"] = *operation.Prompt
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(shot["name"]), Summary: fmt.Sprintf("修改镜头《%s》", stringValue(shot["name"]))}, nil
|
||
case "camera_keyframe_upsert", "camera_keyframe_remove":
|
||
camera := cloudAgentPrevisFindByID(cameras, operation.ID)
|
||
if camera == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标摄影机不存在")
|
||
}
|
||
duration := cloudAgentPrevisShotDuration(scene)
|
||
var err error
|
||
if operation.Type == "camera_keyframe_upsert" {
|
||
err = cloudAgentPrevisUpsertKeyframe(camera, operation, duration)
|
||
} else {
|
||
err = cloudAgentPrevisRemoveKeyframe(camera, operation)
|
||
}
|
||
if err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(camera["name"]), Summary: fmt.Sprintf("修改摄影机《%s》的动画", stringValue(camera["name"]))}, nil
|
||
case "camera_add":
|
||
if cloudAgentPrevisFindByID(cameras, operation.ID) != nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增摄影机 ID 已存在")
|
||
}
|
||
name := "摄影机"
|
||
if operation.Name != nil {
|
||
name = *operation.Name
|
||
}
|
||
if err := cloudAgentPrevisValidateText(name, "摄影机名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
camPos := []float64{4.8, 2.7, 6.8}
|
||
camRot := []float64{0, 0, 0}
|
||
camScale := []float64{1, 1, 1}
|
||
camTarget := []float64{0, 1, 0}
|
||
if operation.Position != nil {
|
||
camPos = *operation.Position
|
||
}
|
||
if operation.Rotation != nil {
|
||
camRot = *operation.Rotation
|
||
}
|
||
if operation.Scale != nil {
|
||
camScale = *operation.Scale
|
||
}
|
||
if operation.Target != nil {
|
||
camTarget = *operation.Target
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(camPos, "摄影机 position"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(camTarget, "摄影机 target"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
focalLength := 35.0
|
||
if operation.FocalLength != nil {
|
||
focalLength = *operation.FocalLength
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(focalLength, "摄影机焦段", 1, 300); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
fov := 2 * math.Atan(36.0/(2*math.Max(1, focalLength))) * 180 / math.Pi
|
||
newCamera := map[string]any{
|
||
"id": operation.ID, "name": name,
|
||
"transform": map[string]any{"position": cloudAgentPrevisVec3(camPos), "rotation": cloudAgentPrevisVec3(camRot), "scale": cloudAgentPrevisVec3(camScale)},
|
||
"target": cloudAgentPrevisVec3(camTarget), "focalLength": focalLength, "fov": fov,
|
||
"aperture": 2.8, "focusDistance": 5.0, "near": 0.05, "far": 500.0, "keyframes": []any{},
|
||
}
|
||
scene["cameras"] = append(cameras, newCamera)
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: name, Summary: fmt.Sprintf("新增摄影机《%s》", name)}, nil
|
||
case "camera_update", "camera_remove":
|
||
camera := cloudAgentPrevisFindByID(cameras, operation.ID)
|
||
if camera == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标摄影机不存在")
|
||
}
|
||
if operation.Type == "camera_remove" {
|
||
if len(cameras) <= 1 {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("预演场景至少保留一台摄影机")
|
||
}
|
||
for _, shot := range shots {
|
||
if stringValue(shot["cameraId"]) == operation.ID {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("摄影机仍被镜头引用,不能删除")
|
||
}
|
||
}
|
||
next := make([]map[string]any, 0, len(cameras)-1)
|
||
for _, item := range cameras {
|
||
if stringValue(item["id"]) != operation.ID {
|
||
next = append(next, item)
|
||
}
|
||
}
|
||
scene["cameras"] = next
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(camera["name"]), Summary: fmt.Sprintf("删除摄影机《%s》", stringValue(camera["name"]))}, nil
|
||
}
|
||
if err := cloudAgentPrevisApplyName(camera, operation.Name, "摄影机名称"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if operation.FocalLength != nil {
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.FocalLength, "摄影机焦段", 1, 300); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
camera["focalLength"] = *operation.FocalLength
|
||
}
|
||
if operation.Target != nil {
|
||
if err := cloudAgentPrevisValidateVec3(*operation.Target, "target"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
camera["target"] = cloudAgentPrevisVec3(*operation.Target)
|
||
}
|
||
if err := cloudAgentPrevisApplyTransform(camera, operation); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(camera["name"]), Summary: fmt.Sprintf("修改摄影机《%s》", stringValue(camera["name"]))}, nil
|
||
case "light_add", "light_update", "light_remove":
|
||
if operation.Type == "light_add" {
|
||
if cloudAgentPrevisFindByID(lights, operation.ID) != nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("新增灯光 ID 已存在")
|
||
}
|
||
name := "新灯光"
|
||
if operation.Name != nil {
|
||
name = *operation.Name
|
||
}
|
||
if err := cloudAgentPrevisValidateText(name, "灯光名称", cloudAgentPrevisTextLimit, true); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
lightType := "point"
|
||
if operation.LightType != nil {
|
||
lightType = *operation.LightType
|
||
}
|
||
if err := cloudAgentPrevisValidateEnum(lightType, "灯光类型", "directional", "point", "spot", "ambient"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
position, rotation, scale := []float64{0, 3, 0}, []float64{0, 0, 0}, []float64{1, 1, 1}
|
||
if operation.Position != nil {
|
||
position = *operation.Position
|
||
}
|
||
if operation.Rotation != nil {
|
||
rotation = *operation.Rotation
|
||
}
|
||
if operation.Scale != nil {
|
||
scale = *operation.Scale
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(position, "position"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(rotation, "rotation"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisValidateVec3(scale, "scale"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
intensity := 1.0
|
||
if operation.Intensity != nil {
|
||
intensity = *operation.Intensity
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(intensity, "灯光强度", 0, 100); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
angle, penumbra := math.Pi/4, 0.35
|
||
if operation.Angle != nil {
|
||
angle = *operation.Angle
|
||
}
|
||
if operation.Penumbra != nil {
|
||
penumbra = *operation.Penumbra
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(angle, "灯光角度", 0, 6.29); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if err := cloudAgentPrevisFiniteInRange(penumbra, "灯光 penumbra", 0, 1); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
color := "#ffffff"
|
||
if operation.Color != nil {
|
||
if err := cloudAgentPrevisValidateColor(*operation.Color, "灯光颜色"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
color = *operation.Color
|
||
}
|
||
castShadow := lightType != "ambient"
|
||
if operation.CastShadow != nil {
|
||
castShadow = *operation.CastShadow
|
||
}
|
||
light := map[string]any{"id": operation.ID, "name": name, "type": lightType, "transform": map[string]any{"position": cloudAgentPrevisVec3(position), "rotation": cloudAgentPrevisVec3(rotation), "scale": cloudAgentPrevisVec3(scale)}, "color": color, "intensity": intensity, "angle": angle, "penumbra": penumbra, "castShadow": castShadow}
|
||
scene["lights"] = append(lights, light)
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: name, Summary: fmt.Sprintf("新增灯光《%s》", name)}, nil
|
||
}
|
||
light := cloudAgentPrevisFindByID(lights, operation.ID)
|
||
if light == nil {
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("目标灯光不存在")
|
||
}
|
||
if operation.Type == "light_remove" {
|
||
next := make([]map[string]any, 0, len(lights)-1)
|
||
for _, item := range lights {
|
||
if stringValue(item["id"]) != operation.ID {
|
||
next = append(next, item)
|
||
}
|
||
}
|
||
scene["lights"] = next
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(light["name"]), Summary: fmt.Sprintf("删除灯光《%s》", stringValue(light["name"]))}, nil
|
||
}
|
||
if err := cloudAgentPrevisApplyName(light, operation.Name, "灯光名称"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
if operation.LightType != nil {
|
||
if err := cloudAgentPrevisValidateEnum(*operation.LightType, "灯光类型", "directional", "point", "spot", "ambient"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
light["type"] = *operation.LightType
|
||
}
|
||
if operation.Intensity != nil {
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.Intensity, "灯光强度", 0, 100); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
light["intensity"] = *operation.Intensity
|
||
}
|
||
if operation.Color != nil {
|
||
if err := cloudAgentPrevisValidateColor(*operation.Color, "灯光颜色"); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
light["color"] = *operation.Color
|
||
}
|
||
if operation.Angle != nil {
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.Angle, "灯光角度", 0, 6.29); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
light["angle"] = *operation.Angle
|
||
}
|
||
if operation.Penumbra != nil {
|
||
if err := cloudAgentPrevisFiniteInRange(*operation.Penumbra, "灯光 penumbra", 0, 1); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
light["penumbra"] = *operation.Penumbra
|
||
}
|
||
if operation.CastShadow != nil {
|
||
light["castShadow"] = *operation.CastShadow
|
||
}
|
||
if err := cloudAgentPrevisApplyTransform(light, operation); err != nil {
|
||
return cloudAgentApprovalPreviewItem{}, err
|
||
}
|
||
return cloudAgentApprovalPreviewItem{Operation: operation.Type, NodeID: operation.ID, NodeTitle: stringValue(light["name"]), Summary: fmt.Sprintf("修改灯光《%s》", stringValue(light["name"]))}, nil
|
||
default:
|
||
return cloudAgentApprovalPreviewItem{}, BadAuthRequest("不支持的预演台语义操作")
|
||
}
|
||
}
|
||
|
||
func applyCloudAgentPrevisMutation(repo *repository.Repository, userID, canvasID string, call cloudAgentCall, policy RuntimePolicySetting, recorder ...cloudAgentMutationRecorder) (any, error) {
|
||
var plan *cloudAgentPrevisMutationPlan
|
||
var err error
|
||
switch call.Function.Name {
|
||
case "previs_scene_create":
|
||
plan, err = prepareCloudAgentPrevisSceneCreate(repo, userID, canvasID, call)
|
||
case "previs_apply_patch":
|
||
plan, err = prepareCloudAgentPrevisApplyPatch(repo, userID, canvasID, call)
|
||
default:
|
||
return nil, BadAuthRequest("不支持的预演台写工具")
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err = saveCloudAgentDocument(repo, plan.Canvas, plan.Document, policy); err != nil {
|
||
return nil, err
|
||
}
|
||
afterCanvasHash := cloudAgentCanvasHash(plan.Document)
|
||
afterSceneHash := plan.ResultSnapshotHash
|
||
if scene, ok := findPrevisScene(creationMaps(plan.Document["previsScenes"]), plan.SceneID); ok {
|
||
afterSceneHash = creationHash(scene)
|
||
}
|
||
if len(recorder) > 0 && recorder[0] != nil {
|
||
preview := plan.Preview
|
||
if err := recorder[0](repo, cloudAgentMutationInput{UserID: userID, CanvasID: canvasID, StepID: call.ID, Operation: plan.Operation, BeforeSnapshotHash: plan.BeforeSnapshotHash, AfterSnapshotHash: afterCanvasHash, BeforeJSON: plan.BeforeJSON, Preview: &preview}); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
resultSnapshotHash := afterSceneHash
|
||
if resultSnapshotHash == "" {
|
||
resultSnapshotHash = afterCanvasHash
|
||
}
|
||
return map[string]any{"canvasId": canvasID, "sceneId": plan.SceneID, "nodeId": plan.NodeID, "shotId": plan.ShotID, "snapshotHash": resultSnapshotHash, "canvasSnapshotHash": afterCanvasHash, "committed": true, "summary": plan.Preview.Description}, nil
|
||
}
|