Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_director.go

273 lines
8.7 KiB
Go
Raw 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 app
import (
"fmt"
"strings"
"infinite-canvas/backend/internal/repository"
)
// Director Agent tools deliberately expose a projection rather than the raw
// DirectorScene payload. The browser owns model URLs and storage references;
// the Agent only needs semantic shot/layout data to plan a preview.
func cloudAgentDirectorSceneRead(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (any, error) {
var args struct {
SceneID string `json:"sceneId"`
ShotID string `json:"shotId"`
ObjectIDs []string `json:"objectIds"`
}
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
return nil, cloudAgentJSONArgumentError(err)
}
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
return nil, err
}
if err := validateCloudAgentID(args.ShotID, "导演镜头 ID", 80); err != nil {
return nil, err
}
if len(args.ObjectIDs) > 16 {
return nil, BadAuthRequest("导演对象读取最多包含16个对象 ID")
}
for _, id := range args.ObjectIDs {
if err := validateCloudAgentID(id, "导演对象 ID", 80); err != nil {
return nil, err
}
}
canvas, err := repo.CanvasProjectForUser(userID, canvasID)
if err != nil {
return nil, err
}
doc, err := creationDocument(canvas.PayloadJSON)
if err != nil {
return nil, err
}
rawScenes := creationMaps(doc["directorScenes"])
if args.SceneID == "" {
items := make([]map[string]any, 0, len(rawScenes))
for _, scene := range rawScenes {
items = append(items, map[string]any{
"id": stringValue(scene["id"]),
"title": truncateRunes(stringValue(scene["title"]), 160),
"activeShotId": stringValue(scene["activeShotId"]),
"shotCount": len(creationMaps(scene["shots"])),
"objectCount": len(creationMaps(scene["objects"])),
})
}
return map[string]any{
"canvasId": canvasID,
"scenes": items,
"count": len(items),
"guidance": "先用返回的 sceneId 读取一个场景,再使用 shotId 进行布局或预演。",
}, nil
}
scene, ok := findDirectorScene(rawScenes, args.SceneID)
if !ok {
return nil, BadAuthRequest("导演场景不存在或不属于当前画布")
}
if args.ShotID != "" {
if _, ok := findDirectorShot(scene, args.ShotID); !ok {
return nil, BadAuthRequest("导演镜头不存在或不属于指定场景")
}
}
objectIDs := make(map[string]bool, len(args.ObjectIDs))
for _, id := range args.ObjectIDs {
objectIDs[id] = true
}
return map[string]any{
"canvasId": canvasID,
"scene": projectDirectorScene(scene, args.ShotID, objectIDs),
"snapshotHash": creationHash(scene),
"revision": canvas.Revision,
"guidance": "这是安全摘要,不是可直接回写的完整 JSON;需要预演时使用 director_preview。",
}, nil
}
func cloudAgentDirectorPreview(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (any, error) {
var args struct {
SceneID string `json:"sceneId"`
ShotID string `json:"shotId"`
Duration *float64 `json:"duration"`
FPS *int `json:"fps"`
Output string `json:"output"`
}
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
return nil, cloudAgentJSONArgumentError(err)
}
if err := validateCloudAgentID(args.SceneID, "导演场景 ID", 80); err != nil {
return nil, err
}
if err := validateCloudAgentID(args.ShotID, "导演镜头 ID", 80); err != nil {
return nil, err
}
if args.Output != "" && args.Output != "clay_video" {
return nil, BadAuthRequest("导演预演 output 目前只支持 clay_video")
}
if args.Duration != nil && (*args.Duration < 0.1 || *args.Duration > 60) {
return nil, BadAuthRequest("导演预演 duration 必须在0.1到60秒之间")
}
if args.FPS != nil && (*args.FPS < 1 || *args.FPS > 60) {
return nil, BadAuthRequest("导演预演 fps 必须在1到60之间")
}
canvas, err := repo.CanvasProjectForUser(userID, canvasID)
if err != nil {
return nil, err
}
doc, err := creationDocument(canvas.PayloadJSON)
if err != nil {
return nil, err
}
scene, ok := findDirectorScene(creationMaps(doc["directorScenes"]), args.SceneID)
if !ok {
return nil, BadAuthRequest("导演场景不存在或不属于当前画布")
}
shot, ok := findDirectorShot(scene, args.ShotID)
if !ok {
return nil, BadAuthRequest("导演镜头不存在或不属于指定场景")
}
duration := numberValue(shot["duration"], 5)
if args.Duration != nil {
duration = *args.Duration
}
fps := int(numberValue(shot["fps"], 24))
if args.FPS != nil {
fps = *args.FPS
}
requestID := strings.TrimSpace(call.ID)
if requestID == "" {
requestID = fmt.Sprintf("director-preview-%s-%s", args.SceneID, args.ShotID)
}
return map[string]any{
"previewRequestId": requestID,
"canvasId": canvasID,
"sceneId": args.SceneID,
"sceneTitle": truncateRunes(stringValue(scene["title"]), 160),
"shotId": args.ShotID,
"shotName": truncateRunes(stringValue(shot["name"]), 160),
"duration": duration,
"fps": fps,
"output": "clay_video",
"snapshotHash": creationHash(scene),
"execution": "browser_director_viewport",
"text": "已请求导演台在浏览器视口录制白模预演;录制完成后会回写为画布视频节点。",
}, nil
}
func findDirectorScene(scenes []map[string]any, id string) (map[string]any, bool) {
for _, scene := range scenes {
if stringValue(scene["id"]) == id {
return scene, true
}
}
return nil, false
}
func findDirectorShot(scene map[string]any, id string) (map[string]any, bool) {
for _, shot := range creationMaps(scene["shots"]) {
if stringValue(shot["id"]) == id {
return shot, true
}
}
return nil, false
}
func projectDirectorScene(scene map[string]any, shotID string, objectIDs map[string]bool) map[string]any {
shots := make([]map[string]any, 0)
for _, shot := range creationMaps(scene["shots"]) {
if shotID != "" && stringValue(shot["id"]) != shotID {
continue
}
shots = append(shots, map[string]any{
"id": stringValue(shot["id"]),
"name": truncateRunes(stringValue(shot["name"]), 160),
"cameraId": stringValue(shot["cameraId"]),
"duration": numberValue(shot["duration"], 5),
"fps": int(numberValue(shot["fps"], 24)),
"shotSize": stringValue(shot["shotSize"]),
"cameraMove": stringValue(shot["cameraMove"]),
})
}
objects := make([]map[string]any, 0)
for _, object := range creationMaps(scene["objects"]) {
id := stringValue(object["id"])
if len(objectIDs) > 0 && !objectIDs[id] {
continue
}
objects = append(objects, map[string]any{
"id": id,
"name": truncateRunes(stringValue(object["name"]), 160),
"kind": stringValue(object["kind"]),
"primitive": stringValue(object["primitive"]),
"color": stringValue(object["color"]),
"pose": stringValue(object["pose"]),
"visible": boolValue(object["visible"], true),
"transform": projectDirectorTransform(object["transform"]),
})
}
cameras := make([]map[string]any, 0)
for _, camera := range creationMaps(scene["cameras"]) {
cameras = append(cameras, map[string]any{
"id": stringValue(camera["id"]),
"name": truncateRunes(stringValue(camera["name"]), 160),
"focalLength": numberValue(camera["focalLength"], 50),
"transform": projectDirectorTransform(camera["transform"]),
"target": projectDirectorVec3(camera["target"]),
})
}
return map[string]any{
"id": stringValue(scene["id"]),
"title": truncateRunes(stringValue(scene["title"]), 160),
"activeShotId": stringValue(scene["activeShotId"]),
"shots": shots,
"objects": objects,
"cameras": cameras,
"lightCount": len(creationMaps(scene["lights"])),
}
}
func projectDirectorTransform(value any) map[string]any {
transform, _ := value.(map[string]any)
return map[string]any{
"position": projectDirectorVec3(transform["position"]),
"rotation": projectDirectorVec3(transform["rotation"]),
"scale": projectDirectorVec3(transform["scale"]),
}
}
func projectDirectorVec3(value any) []any {
items, ok := value.([]any)
if !ok || len(items) != 3 {
return []any{0, 0, 0}
}
result := make([]any, 3)
for index, item := range items {
result[index], _ = cloudAgentSafeNumber(item)
if result[index] == nil {
result[index] = 0
}
}
return result
}
func numberValue(value any, fallback float64) float64 {
if number, ok := cloudAgentSafeNumber(value); ok {
switch typed := number.(type) {
case float64:
return typed
case int:
return float64(typed)
case int64:
return float64(typed)
}
}
return fallback
}
func boolValue(value any, fallback bool) bool {
if result, ok := value.(bool); ok {
return result
}
return fallback
}