@pippit-dev/cli 1.0.48 → 1.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -9
- package/checksums.txt +6 -6
- package/cmd/generate_video/query_result.go +1 -14
- package/cmd/generate_video_test.go +73 -2
- package/dist/checksums.txt +6 -6
- package/dist/xyq-canvas-command-runtime.cjs +14 -14
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +1 -27
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -2
- package/internal/canvas/image_binding.go +152 -0
- package/internal/canvas/image_binding_test.go +137 -0
- package/internal/canvas/media_identity.go +21 -12
- package/internal/generate_video/query_result.go +80 -23
- package/package.json +1 -1
- package/scripts/canvas-command-guides.js +13 -21
- package/scripts/canvas-command.js +11 -0
- package/skills/xyq-nest-skill/commands/canvas.md +6 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
XYQ Canvas Command Runtime - Bundled Software Notices
|
|
2
2
|
|
|
3
3
|
Generated from the exact esbuild input graph. No build paths or timestamps are included.
|
|
4
|
-
|
|
4
|
+
14 Pippit first-party modules are bundled and are not listed by internal package name.
|
|
5
5
|
|
|
6
6
|
===== @babel/runtime@7.29.7 =====
|
|
7
7
|
License metadata: MIT
|
|
@@ -546,32 +546,6 @@ maintained libraries used by this software which have their own
|
|
|
546
546
|
licenses; we recommend you read them, as their terms may differ from the
|
|
547
547
|
terms above.
|
|
548
548
|
|
|
549
|
-
===== three@0.184.0 =====
|
|
550
|
-
License metadata: MIT
|
|
551
|
-
|
|
552
|
-
--- LICENSE ---
|
|
553
|
-
The MIT License
|
|
554
|
-
|
|
555
|
-
Copyright © 2010-2026 three.js authors
|
|
556
|
-
|
|
557
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
558
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
559
|
-
in the Software without restriction, including without limitation the rights
|
|
560
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
561
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
562
|
-
furnished to do so, subject to the following conditions:
|
|
563
|
-
|
|
564
|
-
The above copyright notice and this permission notice shall be included in
|
|
565
|
-
all copies or substantial portions of the Software.
|
|
566
|
-
|
|
567
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
568
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
569
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
570
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
571
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
572
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
573
|
-
THE SOFTWARE.
|
|
574
|
-
|
|
575
549
|
===== tslib@2.6.2 =====
|
|
576
550
|
License metadata: 0BSD
|
|
577
551
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
1c53a5f869ee7c148e9f993e318d4ea5b34c5e93e2ac0749dd3f47ecff946e4f xyq-canvas-command-runtime.cjs
|
|
2
|
+
c547c923eaf2e8c155f84aed934f42ad02855d669e8c0a890e38d049bc1ef8e7 xyq-canvas-command-runtime.cjs.LEGAL.txt
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
package canvas
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"reflect"
|
|
8
|
+
"strconv"
|
|
9
|
+
"strings"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
var imageSubjectFields = []string{"appearanceinfo", "sourceinfos"}
|
|
13
|
+
|
|
14
|
+
func isImageSubjectField(field string) bool {
|
|
15
|
+
name := strings.ToLower(field)
|
|
16
|
+
return name == imageSubjectFields[0] || name == imageSubjectFields[1]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func nodeBindingContent(node map[string]json.RawMessage) map[string]json.RawMessage {
|
|
20
|
+
raw, _ := identityField(node, "content")
|
|
21
|
+
var content map[string]json.RawMessage
|
|
22
|
+
_ = json.Unmarshal(raw, &content)
|
|
23
|
+
return content
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func decodeBindingValue(raw json.RawMessage) any {
|
|
27
|
+
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
28
|
+
decoder.UseNumber()
|
|
29
|
+
var value any
|
|
30
|
+
_ = decoder.Decode(&value)
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func hasSubjectBinding(value any) bool {
|
|
35
|
+
switch value := value.(type) {
|
|
36
|
+
case nil:
|
|
37
|
+
return false
|
|
38
|
+
case string:
|
|
39
|
+
return strings.TrimSpace(value) != ""
|
|
40
|
+
case []any:
|
|
41
|
+
return len(value) > 0
|
|
42
|
+
case map[string]any:
|
|
43
|
+
return len(value) > 0
|
|
44
|
+
default:
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func replacesImageSubjectBinding(patch PatchEntry) bool {
|
|
50
|
+
if patch.Path != "" || patch.Op != "replace" {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
var node map[string]json.RawMessage
|
|
54
|
+
if json.Unmarshal(patch.Value, &node) != nil {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
kind, _ := identityString(node, "type")
|
|
58
|
+
if kind != "biz/image" {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
for field, raw := range nodeBindingContent(node) {
|
|
62
|
+
if isImageSubjectField(field) && hasSubjectBinding(decodeBindingValue(raw)) {
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Legacy fields may be retained or cleared, but new image bindings use the
|
|
70
|
+
// image protocol. Do not infer a role/scene conversion from arbitrary content.
|
|
71
|
+
func validateImageSubjectBinding(nodeID string, node, previous map[string]json.RawMessage) error {
|
|
72
|
+
content := nodeBindingContent(node)
|
|
73
|
+
oldContent := nodeBindingContent(previous)
|
|
74
|
+
oldType, _ := identityString(previous, "type")
|
|
75
|
+
for field, raw := range content {
|
|
76
|
+
if !isImageSubjectField(field) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
value := decodeBindingValue(raw)
|
|
80
|
+
if !hasSubjectBinding(value) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if oldType == "biz/image" && reflect.DeepEqual(value, decodeBindingValue(oldContent[field])) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
return mediaIdentityError(nodeID, "/content/"+field,
|
|
87
|
+
"biz/image cannot bind role/scene content; use content.pippitAssetId for an image or a biz/role or biz/scene node")
|
|
88
|
+
}
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Project only the selected identity subtree, including nested subject fields.
|
|
93
|
+
// RFC 6902 array operations must not collapse a child patch into its parent.
|
|
94
|
+
func projectMediaIdentityValue(raw json.RawMessage, parts []string, patch PatchEntry) (json.RawMessage, error) {
|
|
95
|
+
if len(parts) == 0 {
|
|
96
|
+
return patch.Value, nil
|
|
97
|
+
}
|
|
98
|
+
value := bytes.TrimSpace(raw)
|
|
99
|
+
if len(value) == 0 {
|
|
100
|
+
return nil, fmt.Errorf("cannot validate a media identity without its content base")
|
|
101
|
+
}
|
|
102
|
+
key := parts[0]
|
|
103
|
+
if value[0] == '{' {
|
|
104
|
+
var object map[string]json.RawMessage
|
|
105
|
+
if err := json.Unmarshal(value, &object); err != nil {
|
|
106
|
+
return nil, err
|
|
107
|
+
}
|
|
108
|
+
if len(parts) == 1 && patch.Op == "remove" {
|
|
109
|
+
delete(object, key)
|
|
110
|
+
} else {
|
|
111
|
+
next, err := projectMediaIdentityValue(object[key], parts[1:], patch)
|
|
112
|
+
if err != nil {
|
|
113
|
+
return nil, err
|
|
114
|
+
}
|
|
115
|
+
object[key] = next
|
|
116
|
+
}
|
|
117
|
+
return json.Marshal(object)
|
|
118
|
+
}
|
|
119
|
+
if value[0] != '[' {
|
|
120
|
+
return nil, fmt.Errorf("cannot validate a nested identity patch on a scalar")
|
|
121
|
+
}
|
|
122
|
+
var array []json.RawMessage
|
|
123
|
+
if err := json.Unmarshal(value, &array); err != nil {
|
|
124
|
+
return nil, err
|
|
125
|
+
}
|
|
126
|
+
index, err := strconv.Atoi(key)
|
|
127
|
+
if key == "-" && len(parts) == 1 && patch.Op == "add" {
|
|
128
|
+
index, err = len(array), nil
|
|
129
|
+
}
|
|
130
|
+
if err != nil || index < 0 || index > len(array) || (len(key) > 1 && key[0] == '0') {
|
|
131
|
+
return nil, fmt.Errorf("invalid media identity array index %q", key)
|
|
132
|
+
}
|
|
133
|
+
if len(parts) == 1 && patch.Op == "add" {
|
|
134
|
+
array = append(array, nil)
|
|
135
|
+
copy(array[index+1:], array[index:])
|
|
136
|
+
array[index] = patch.Value
|
|
137
|
+
} else {
|
|
138
|
+
if index == len(array) {
|
|
139
|
+
return nil, fmt.Errorf("media identity array index %q does not exist", key)
|
|
140
|
+
}
|
|
141
|
+
if len(parts) == 1 && patch.Op == "remove" {
|
|
142
|
+
array = append(array[:index], array[index+1:]...)
|
|
143
|
+
} else {
|
|
144
|
+
next, err := projectMediaIdentityValue(array[index], parts[1:], patch)
|
|
145
|
+
if err != nil {
|
|
146
|
+
return nil, err
|
|
147
|
+
}
|
|
148
|
+
array[index] = next
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return json.Marshal(array)
|
|
152
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
package canvas
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func imageBindingFixture(node string, patches []PatchEntry) (ApplyOptions, *common.Runner, *int) {
|
|
14
|
+
opts, _, writes := applyIdentityFixture(patches, imageMaterial())
|
|
15
|
+
client := &fakeClient{send: func(_ context.Context, path string, body, out any) error {
|
|
16
|
+
switch path {
|
|
17
|
+
case QueryPath:
|
|
18
|
+
request := body.(queryRequest)
|
|
19
|
+
var assets []json.RawMessage
|
|
20
|
+
for _, id := range request.PippitAssetIDs {
|
|
21
|
+
if id == testNodeID {
|
|
22
|
+
content, _ := json.Marshal(node)
|
|
23
|
+
assets = append(assets, json.RawMessage(fmt.Sprintf(`{"PippitAssetID":"%s","TextInfo":{"content":%s},"version":5}`, id, content)))
|
|
24
|
+
} else if id == testPippitID {
|
|
25
|
+
assets = append(assets, json.RawMessage(imageMaterial()))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
payload, _ := json.Marshal(map[string]any{"ret": "0", "data": map[string]any{"Assets": assets}})
|
|
29
|
+
return decodeInto(out, string(payload))
|
|
30
|
+
case ApplyPath:
|
|
31
|
+
*writes++
|
|
32
|
+
return decodeInto(out, fmt.Sprintf(`{"ret":"0","data":{"results":[{"transaction_id":"tx","status":"ack","asset_versions":{"%s":6}}]}}`, testNodeID))
|
|
33
|
+
default:
|
|
34
|
+
return fmt.Errorf("unexpected request %s", path)
|
|
35
|
+
}
|
|
36
|
+
}}
|
|
37
|
+
return opts, runnerWithClient(client), writes
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func bindingPatch(op, path, value string) PatchEntry {
|
|
41
|
+
return PatchEntry{AssetID: testNodeID, Op: op, Path: path, Value: json.RawMessage(value)}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestApplyMediaIdentityRejectsRoleFieldsOnImageBeforeWrite(t *testing.T) {
|
|
45
|
+
blank := `{"type":"biz/image","content":{"caption":"draft","generation":{}}}`
|
|
46
|
+
for _, test := range []struct {
|
|
47
|
+
name, node string
|
|
48
|
+
patches []PatchEntry
|
|
49
|
+
}{
|
|
50
|
+
{"original-upload", blank, []PatchEntry{
|
|
51
|
+
bindingPatch("add", "/content/appearanceInfo", `{"pippitAssetId":"image-material"}`),
|
|
52
|
+
bindingPatch("add", "/content/generation", `{"source":"uploaded"}`),
|
|
53
|
+
bindingPatch("add", "/content/sourceInfos", `{"image-material":{"source":"uploaded"}}`),
|
|
54
|
+
}},
|
|
55
|
+
{"source-map", blank, []PatchEntry{bindingPatch("add", "/content/sourceInfos", `{"image-material":{}}`)}},
|
|
56
|
+
{"source-array", blank, []PatchEntry{bindingPatch("add", "/content/sourceInfos", `[{"pippitAssetId":"image-material"}]`)}},
|
|
57
|
+
{"root-create", "", []PatchEntry{bindingPatch("add", "", `{"type":"biz/image","content":{"appearanceInfo":{"pippitAssetId":"image-material"}}}`)}},
|
|
58
|
+
{"root-replace", blank, []PatchEntry{bindingPatch("replace", "", `{"type":"biz/image","content":{"sourceInfos":{"image-material":{}}}}`)}},
|
|
59
|
+
{"content-replace", blank, []PatchEntry{bindingPatch("replace", "/content", `{"appearanceInfo":{"candidateId":"candidate"}}`)}},
|
|
60
|
+
{"nested-appearance", `{"type":"biz/image","content":{"appearanceInfo":{}}}`, []PatchEntry{bindingPatch("add", "/content/appearanceInfo/pippitAssetId", `"image-material"`)}},
|
|
61
|
+
{"nested-source-map", `{"type":"biz/image","content":{"sourceInfos":{}}}`, []PatchEntry{bindingPatch("add", "/content/sourceInfos/material", `{"pippitAssetId":"image-material"}`)}},
|
|
62
|
+
{"nested-source-array", `{"type":"biz/image","content":{"sourceInfos":[]}}`, []PatchEntry{bindingPatch("add", "/content/sourceInfos/-", `{"pippitAssetId":"image-material"}`)}},
|
|
63
|
+
{"changed-legacy-field", `{"type":"biz/image","content":{"appearanceInfo":{"pippitAssetId":"old"}}}`, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo/pippitAssetId", `"new"`)}},
|
|
64
|
+
{"remove-one-legacy-source", `{"type":"biz/image","content":{"sourceInfos":{"first":{},"second":{}}}}`, []PatchEntry{bindingPatch("remove", "/content/sourceInfos/first", "")}},
|
|
65
|
+
{"unchanged-direct-identities", string(imageNode(testMediaID)), []PatchEntry{bindingPatch("add", "/content/sourceInfos", `{"material":{}}`)}},
|
|
66
|
+
{"type-change", `{"type":"biz/role","content":{"appearanceInfo":{"pippitAssetId":"material"}}}`, []PatchEntry{bindingPatch("replace", "/type", `"biz/image"`)}},
|
|
67
|
+
{"case-alias", blank, []PatchEntry{bindingPatch("add", "/content/AppearanceInfo", `{"pippitAssetId":"material"}`)}},
|
|
68
|
+
} {
|
|
69
|
+
t.Run(test.name, func(t *testing.T) {
|
|
70
|
+
opts, runner, writes := imageBindingFixture(test.node, test.patches)
|
|
71
|
+
_, err := Apply(context.Background(), opts, runner)
|
|
72
|
+
if err == nil || !strings.Contains(err.Error(), "INVALID_MEDIA_IDENTITY") || !strings.Contains(err.Error(), "/content/") || !strings.Contains(err.Error(), "content.pippitAssetId") {
|
|
73
|
+
t.Fatalf("expected corrective image binding error, got %v", err)
|
|
74
|
+
}
|
|
75
|
+
if *writes != 0 {
|
|
76
|
+
t.Fatalf("invalid image binding was sent %d times", *writes)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestApplyMediaIdentityPreservesImageDraftAndLegacyEdits(t *testing.T) {
|
|
83
|
+
legacy := `{"type":"biz/image","content":{"caption":"old","appearanceInfo":{"pippitAssetId":"material","candidateId":"candidate"},"sourceInfos":{"material":{}}}}`
|
|
84
|
+
for _, test := range []struct {
|
|
85
|
+
name, node string
|
|
86
|
+
patches []PatchEntry
|
|
87
|
+
}{
|
|
88
|
+
{"blank-create", "", []PatchEntry{bindingPatch("add", "", `{"type":"biz/image","content":{"caption":"draft"}}`)}},
|
|
89
|
+
{"legacy-caption", legacy, []PatchEntry{bindingPatch("replace", "/content/caption", `"edited"`)}},
|
|
90
|
+
{"unchanged-content", legacy, []PatchEntry{bindingPatch("replace", "/content", `{"sourceInfos":{"material":{}},"appearanceInfo":{"candidateId":"candidate","pippitAssetId":"material"},"caption":"edited"}`)}},
|
|
91
|
+
{"unchanged-root", legacy, []PatchEntry{bindingPatch("replace", "", `{"type":"biz/image","content":{"sourceInfos":{"material":{}},"appearanceInfo":{"candidateId":"candidate","pippitAssetId":"material"},"caption":"edited"}}`)}},
|
|
92
|
+
{"unchanged-nested-field", legacy, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo/pippitAssetId", `"material"`)}},
|
|
93
|
+
{"remove-field", legacy, []PatchEntry{bindingPatch("remove", "/content/appearanceInfo", "")}},
|
|
94
|
+
{"clear-object", legacy, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo", `{}`)}},
|
|
95
|
+
{"clear-array", legacy, []PatchEntry{bindingPatch("replace", "/content/sourceInfos", `[]`)}},
|
|
96
|
+
{"clear-null", legacy, []PatchEntry{bindingPatch("replace", "/content/sourceInfos", `null`)}},
|
|
97
|
+
{"clear-blank", legacy, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo", `" "`)}},
|
|
98
|
+
{"clear-last-object-key", `{"type":"biz/image","content":{"sourceInfos":{"a/b~c":{}}}}`, []PatchEntry{bindingPatch("remove", "/content/sourceInfos/a~1b~0c", "")}},
|
|
99
|
+
{"clear-last-array-item", `{"type":"biz/image","content":{"sourceInfos":[{"pippitAssetId":"material"}]}}`, []PatchEntry{bindingPatch("remove", "/content/sourceInfos/0", "")}},
|
|
100
|
+
{"final-role-type", `{"type":"biz/image","content":{}}`, []PatchEntry{
|
|
101
|
+
bindingPatch("add", "/content/appearanceInfo", `{"pippitAssetId":"material"}`),
|
|
102
|
+
bindingPatch("replace", "/type", `"biz/role"`),
|
|
103
|
+
}},
|
|
104
|
+
{"final-empty-binding", `{"type":"biz/image","content":{}}`, []PatchEntry{
|
|
105
|
+
bindingPatch("add", "/content/appearanceInfo", `{"pippitAssetId":"material"}`),
|
|
106
|
+
bindingPatch("remove", "/content/appearanceInfo", ""),
|
|
107
|
+
}},
|
|
108
|
+
} {
|
|
109
|
+
t.Run(test.name, func(t *testing.T) {
|
|
110
|
+
opts, runner, writes := imageBindingFixture(test.node, test.patches)
|
|
111
|
+
if _, err := Apply(context.Background(), opts, runner); err != nil {
|
|
112
|
+
t.Fatal(err)
|
|
113
|
+
}
|
|
114
|
+
if *writes != 1 {
|
|
115
|
+
t.Fatalf("writes = %d", *writes)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func TestApplyMediaIdentityLeavesOtherNodeBindingsUnchanged(t *testing.T) {
|
|
122
|
+
for _, kind := range []string{"role", "scene", "audio", "video"} {
|
|
123
|
+
t.Run(kind, func(t *testing.T) {
|
|
124
|
+
node := fmt.Sprintf(`{"type":"biz/%s","content":{"appearanceInfo":{},"sourceInfos":[]}}`, kind)
|
|
125
|
+
opts, runner, writes := imageBindingFixture(node, []PatchEntry{
|
|
126
|
+
bindingPatch("add", "/content/appearanceInfo/pippitAssetId", `"material"`),
|
|
127
|
+
bindingPatch("add", "/content/sourceInfos/-", `{"pippitAssetId":"material"}`),
|
|
128
|
+
})
|
|
129
|
+
if _, err := Apply(context.Background(), opts, runner); err != nil {
|
|
130
|
+
t.Fatal(err)
|
|
131
|
+
}
|
|
132
|
+
if *writes != 1 {
|
|
133
|
+
t.Fatalf("writes = %d", *writes)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -4,6 +4,7 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"encoding/json"
|
|
6
6
|
"fmt"
|
|
7
|
+
"maps"
|
|
7
8
|
"strings"
|
|
8
9
|
|
|
9
10
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
@@ -91,10 +92,17 @@ func cloudMediaID(raw json.RawMessage, kind string) (string, error) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
func touchesMediaIdentity(path string) bool {
|
|
94
|
-
|
|
95
|
+
normalized := strings.ToLower(path)
|
|
96
|
+
switch normalized {
|
|
95
97
|
case "", "/type", "/content", "/content/assetid", "/content/pippitassetid":
|
|
96
98
|
return true
|
|
97
99
|
}
|
|
100
|
+
for _, field := range imageSubjectFields {
|
|
101
|
+
prefix := "/content/" + field
|
|
102
|
+
if normalized == prefix || strings.HasPrefix(normalized, prefix+"/") {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
}
|
|
98
106
|
return false
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -143,6 +151,9 @@ func projectIdentityPatch(node map[string]json.RawMessage, patch PatchEntry) (ma
|
|
|
143
151
|
return nil, fmt.Errorf("cannot validate a patch without its node base")
|
|
144
152
|
}
|
|
145
153
|
parts := strings.Split(strings.TrimPrefix(patch.Path, "/"), "/")
|
|
154
|
+
for index, part := range parts {
|
|
155
|
+
parts[index] = strings.NewReplacer("~1", "/", "~0", "~").Replace(part)
|
|
156
|
+
}
|
|
146
157
|
key := parts[0]
|
|
147
158
|
if len(parts) == 1 {
|
|
148
159
|
if patch.Op == "remove" {
|
|
@@ -152,16 +163,7 @@ func projectIdentityPatch(node map[string]json.RawMessage, patch PatchEntry) (ma
|
|
|
152
163
|
}
|
|
153
164
|
return node, nil
|
|
154
165
|
}
|
|
155
|
-
|
|
156
|
-
if err := json.Unmarshal(node[key], &content); err != nil || content == nil {
|
|
157
|
-
return nil, fmt.Errorf("cannot validate a media identity without its content base")
|
|
158
|
-
}
|
|
159
|
-
if patch.Op == "remove" {
|
|
160
|
-
delete(content, parts[1])
|
|
161
|
-
} else {
|
|
162
|
-
content[parts[1]] = patch.Value
|
|
163
|
-
}
|
|
164
|
-
value, err := json.Marshal(content)
|
|
166
|
+
value, err := projectMediaIdentityValue(node[key], parts[1:], patch)
|
|
165
167
|
if err != nil {
|
|
166
168
|
return nil, err
|
|
167
169
|
}
|
|
@@ -210,12 +212,13 @@ func validateApplyMediaIdentities(ctx context.Context, request ApplyRequest, run
|
|
|
210
212
|
if touchesMediaIdentity(patch.Path) && !affected[patch.AssetID] {
|
|
211
213
|
affected[patch.AssetID] = true
|
|
212
214
|
ordered = append(ordered, patch.AssetID)
|
|
213
|
-
if patch.Path != "" {
|
|
215
|
+
if patch.Path != "" || replacesImageSubjectBinding(patch) {
|
|
214
216
|
toQuery = append(toQuery, patch.AssetID)
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
nodes := make(map[string]map[string]json.RawMessage)
|
|
221
|
+
previousNodes := make(map[string]map[string]json.RawMessage)
|
|
219
222
|
type identity struct{ nodeID, kind, pippitID, mediaID string }
|
|
220
223
|
previous := make(map[string]identity)
|
|
221
224
|
if len(toQuery) > 0 {
|
|
@@ -230,6 +233,7 @@ func validateApplyMediaIdentities(ctx context.Context, request ApplyRequest, run
|
|
|
230
233
|
return mediaIdentityError(id, "/content", err.Error())
|
|
231
234
|
}
|
|
232
235
|
nodes[id] = node
|
|
236
|
+
previousNodes[id] = maps.Clone(node)
|
|
233
237
|
kind, pippitID, mediaID, readErr := readNodeIdentity(node)
|
|
234
238
|
if readErr == nil {
|
|
235
239
|
previous[id] = identity{id, kind, pippitID, mediaID}
|
|
@@ -254,6 +258,11 @@ func validateApplyMediaIdentities(ctx context.Context, request ApplyRequest, run
|
|
|
254
258
|
if err != nil {
|
|
255
259
|
return mediaIdentityError(nodeID, "/content", err.Error())
|
|
256
260
|
}
|
|
261
|
+
if kind == "image" {
|
|
262
|
+
if err := validateImageSubjectBinding(nodeID, nodes[nodeID], previousNodes[nodeID]); err != nil {
|
|
263
|
+
return err
|
|
264
|
+
}
|
|
265
|
+
}
|
|
257
266
|
if kind == "" || (pippitID == "" && mediaID == "") {
|
|
258
267
|
continue
|
|
259
268
|
}
|
|
@@ -14,10 +14,54 @@ import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
const (
|
|
17
|
-
successRunState
|
|
18
|
-
failedRunState
|
|
17
|
+
successRunState = 3
|
|
18
|
+
failedRunState = 4
|
|
19
|
+
canceledRunState = 5
|
|
19
20
|
)
|
|
20
21
|
|
|
22
|
+
// Query-result status values. "completed" stays a terminal flag; "status"
|
|
23
|
+
// carries the machine-readable result so integrators need not parse text.
|
|
24
|
+
const (
|
|
25
|
+
statusRunning = "running"
|
|
26
|
+
statusSucceeded = "succeeded"
|
|
27
|
+
statusFailed = "failed"
|
|
28
|
+
statusCanceled = "canceled"
|
|
29
|
+
statusUnknown = "unknown"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// runStateStatus maps the server-side run state (0-9) to the user-facing
|
|
33
|
+
// status. Any unmapped value falls back to unknown instead of being guessed.
|
|
34
|
+
var runStateStatus = map[int]string{
|
|
35
|
+
0: statusUnknown, // Unspecified
|
|
36
|
+
1: statusRunning, // Submitted
|
|
37
|
+
2: statusRunning, // Working
|
|
38
|
+
3: statusSucceeded,
|
|
39
|
+
4: statusFailed,
|
|
40
|
+
5: statusCanceled,
|
|
41
|
+
6: statusUnknown, // InputRequired
|
|
42
|
+
7: statusRunning, // Generating
|
|
43
|
+
8: statusRunning, // Interrupt
|
|
44
|
+
9: statusUnknown, // HITL_Interrupt
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var terminalRunStates = map[int]struct{}{
|
|
48
|
+
successRunState: {},
|
|
49
|
+
failedRunState: {},
|
|
50
|
+
canceledRunState: {},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func statusForRunState(state int) string {
|
|
54
|
+
if s, ok := runStateStatus[state]; ok {
|
|
55
|
+
return s
|
|
56
|
+
}
|
|
57
|
+
return statusUnknown
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func isTerminalRunState(state int) bool {
|
|
61
|
+
_, ok := terminalRunStates[state]
|
|
62
|
+
return ok
|
|
63
|
+
}
|
|
64
|
+
|
|
21
65
|
// QueryResultOptions is the command-facing request shape for query-result.
|
|
22
66
|
type QueryResultOptions struct {
|
|
23
67
|
ThreadID string
|
|
@@ -27,6 +71,7 @@ type QueryResultOptions struct {
|
|
|
27
71
|
|
|
28
72
|
// QueryResultResult describes the user-facing query-result outcome.
|
|
29
73
|
type QueryResultResult struct {
|
|
74
|
+
Status string `json:"status,omitempty"`
|
|
30
75
|
Completed bool `json:"completed"`
|
|
31
76
|
ThreadID string `json:"thread_id"`
|
|
32
77
|
RunID string `json:"run_id"`
|
|
@@ -112,10 +157,7 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
|
|
|
112
157
|
RunID: opts.RunID,
|
|
113
158
|
}, runner)
|
|
114
159
|
if err != nil {
|
|
115
|
-
|
|
116
|
-
return result, nil
|
|
117
|
-
}
|
|
118
|
-
return nil, fmt.Errorf("查询失败:%w", err)
|
|
160
|
+
return nil, queryResultQueryError(err)
|
|
119
161
|
}
|
|
120
162
|
|
|
121
163
|
thread, err := parseQueryThread(threadResult)
|
|
@@ -129,7 +171,8 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
|
|
|
129
171
|
}
|
|
130
172
|
if run.State != successRunState {
|
|
131
173
|
result := &QueryResultResult{
|
|
132
|
-
|
|
174
|
+
Status: statusForRunState(run.State),
|
|
175
|
+
Completed: isTerminalRunState(run.State),
|
|
133
176
|
ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
|
|
134
177
|
RunID: opts.RunID,
|
|
135
178
|
Videos: []QueryResultVideo{},
|
|
@@ -209,6 +252,7 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
|
|
|
209
252
|
}
|
|
210
253
|
|
|
211
254
|
return &QueryResultResult{
|
|
255
|
+
Status: statusSucceeded,
|
|
212
256
|
Completed: true,
|
|
213
257
|
ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
|
|
214
258
|
RunID: opts.RunID,
|
|
@@ -217,26 +261,39 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
|
|
|
217
261
|
}, nil
|
|
218
262
|
}
|
|
219
263
|
|
|
220
|
-
|
|
264
|
+
// queryResultQueryError converts a get_thread failure into a Go error while
|
|
265
|
+
// keeping a clean, customer-facing message. The command layer maps it to
|
|
266
|
+
// status=unknown; the technical envelope stays in the local error log.
|
|
267
|
+
func queryResultQueryError(err error) error {
|
|
221
268
|
var logErr *common.LogIDError
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
269
|
+
if errors.As(err, &logErr) {
|
|
270
|
+
message := getThreadBusinessErrorMessage(logErr.Message)
|
|
271
|
+
if message == "" {
|
|
272
|
+
message = "查询失败"
|
|
273
|
+
}
|
|
274
|
+
if logID := logErr.LogID(); logID != "" {
|
|
275
|
+
return fmt.Errorf("%s log_id=%s", message, logID)
|
|
276
|
+
}
|
|
277
|
+
return fmt.Errorf("%s", message)
|
|
231
278
|
}
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
279
|
+
return fmt.Errorf("查询失败:%w", err)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// NewQueryResultError builds the user-facing result for a failed query. A
|
|
283
|
+
// failed call never produces a run status, so it reports unknown rather than
|
|
284
|
+
// guessing; the concrete cause stays in error_message.
|
|
285
|
+
func NewQueryResultError(err error, opts *QueryResultOptions) *QueryResultResult {
|
|
286
|
+
result := &QueryResultResult{
|
|
287
|
+
Status: statusUnknown,
|
|
288
|
+
ErrorMessage: err.Error(),
|
|
237
289
|
Videos: []QueryResultVideo{},
|
|
238
290
|
Images: []QueryResultImage{},
|
|
239
|
-
}
|
|
291
|
+
}
|
|
292
|
+
if opts != nil {
|
|
293
|
+
result.ThreadID = strings.TrimSpace(opts.ThreadID)
|
|
294
|
+
result.RunID = strings.TrimSpace(opts.RunID)
|
|
295
|
+
}
|
|
296
|
+
return result
|
|
240
297
|
}
|
|
241
298
|
|
|
242
299
|
func getThreadBusinessErrorMessage(message string) string {
|
package/package.json
CHANGED
|
@@ -76,13 +76,13 @@ const GUIDES = [
|
|
|
76
76
|
"多轨 TimelineDraft 的时间字段使用整数微秒:1 秒 = 1000000 µs。输出 width/height 使用像素。草稿 duration 由片段推导;frameRate 当前只读,没有设置帧率 command。expectedRevision 是草稿版本号,不是时间。",
|
|
77
77
|
"3D 关键帧与动作片段 startFrame/durationFrames 使用帧,timeline.fps 定义帧时长;move_keyframes.deltaFrames 是整数帧偏移。动作片段 trimStart/trimEnd 使用源动画秒数,可带小数,不是帧。",
|
|
78
78
|
"通用 generate-video --duration 使用整数秒,由该视频入口处理;它不会在本地把故事板 duration-ms 标签自动转换成该参数。其模型/时长能力不能替代故事板当前模型约束。",
|
|
79
|
-
"ID 从所属查询取得:Timeline 的轨道/片段/来源 ID 由 xyq.timeline.query
|
|
79
|
+
"ID 从所属查询取得:Timeline 的轨道/片段/来源 ID 由 xyq.timeline.query 返回;当前 CLI 未提供 3D V2 内部对象编辑入口。外层 nodeId 始终从当前 Canvas 节点查询取得。",
|
|
80
80
|
].join("\n\n"),
|
|
81
81
|
next_commands: [
|
|
82
82
|
"pippit-tool-cli canvas command guide storyboard",
|
|
83
83
|
"pippit-tool-cli canvas command guide timeline",
|
|
84
84
|
"pippit-tool-cli canvas command guide scene3d",
|
|
85
|
-
"pippit-tool-cli canvas command
|
|
85
|
+
"pippit-tool-cli canvas command list",
|
|
86
86
|
],
|
|
87
87
|
examples: [
|
|
88
88
|
{
|
|
@@ -125,31 +125,21 @@ const GUIDES = [
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
name: "scene3d",
|
|
128
|
-
summary: "3D
|
|
128
|
+
summary: "3D V2 文档的能力边界、空间与动画单位。",
|
|
129
129
|
content: [
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"关键帧与 startFrame/durationFrames 使用帧,帧时长取决于 timeline.fps;动作片段 trimStart/trimEnd 使用源动画秒数,可为小数。不要把微秒、毫秒或旋转弧度填入帧或角度字段。",
|
|
135
|
-
"这套命令编辑导演文档;截图、渲染导出、上传与生成需要相应运行环境和现有入口。资源/动作引用应来自已查询到的真实资源,不用临时 URL 或虚构 ID 代替。",
|
|
130
|
+
"当前 SDK 主干的 3D 导演文档已迁移到 V2,旧 xyq.scene3d.apply/query 入口已移除。本版本不提供内部对象编辑命令,也不能通过通用资产补丁绕过该边界。内部编辑请使用 Web 3D 导演台。",
|
|
131
|
+
"外层节点仍可通过 create_biz_node 的 nodeKind: scene3d 创建;参数以 describe --node-kind 和当前 schema 为准。",
|
|
132
|
+
"transform.position 使用米,rotation 为 XYZ Euler 角度,scale 为无单位 XYZ 倍率;camera FOV 使用角度。几何体 theta/phi/arc 等参数使用弧度。",
|
|
133
|
+
"关键帧使用帧,帧时长取决于 timeline.fps;动作片段 trimStart/trimEnd 使用源动画秒数。此处仅说明单位,不代表 CLI 已提供编辑能力。",
|
|
136
134
|
].join("\n\n"),
|
|
137
135
|
next_commands: [
|
|
136
|
+
"pippit-tool-cli canvas command list",
|
|
138
137
|
"pippit-tool-cli canvas command describe create_biz_node --node-kind scene3d",
|
|
139
|
-
'pippit-tool-cli canvas command run xyq.scene3d.query --canvas-id CANVAS_ID --input \'{"nodeId":"DIRECTOR_NODE_ID"}\'',
|
|
140
|
-
"pippit-tool-cli canvas command describe xyq.scene3d.apply --operation create_node",
|
|
141
|
-
"pippit-tool-cli canvas command schema xyq.scene3d.apply",
|
|
142
138
|
],
|
|
143
139
|
examples: [
|
|
144
140
|
{
|
|
145
|
-
description: "
|
|
146
|
-
command:
|
|
147
|
-
"pippit-tool-cli canvas command run xyq.scene3d.apply --canvas-id CANVAS_ID --file ./scene3d-input.json",
|
|
148
|
-
input: {
|
|
149
|
-
nodeId: "DIRECTOR_NODE_ID",
|
|
150
|
-
operations: [{ command: "create_node", args: { kind: "camera", id: "camera-new", name: "Close-up" } }],
|
|
151
|
-
dryRun: true,
|
|
152
|
-
},
|
|
141
|
+
description: "只查询外层 3D 节点的创建 schema,不提交文档编辑。",
|
|
142
|
+
command: "pippit-tool-cli canvas command describe create_biz_node --node-kind scene3d",
|
|
153
143
|
},
|
|
154
144
|
],
|
|
155
145
|
related: ["time", "timeline"],
|
|
@@ -163,7 +153,9 @@ function listGuides() {
|
|
|
163
153
|
function getGuide(name) {
|
|
164
154
|
const guide = GUIDES.find((item) => item.name === name);
|
|
165
155
|
if (!guide) {
|
|
166
|
-
throw new Error(
|
|
156
|
+
throw new Error(
|
|
157
|
+
`未知的画布指南:${String(name)}。运行 pippit-tool-cli canvas command guide 查看主题。`,
|
|
158
|
+
);
|
|
167
159
|
}
|
|
168
160
|
return JSON.parse(JSON.stringify(guide));
|
|
169
161
|
}
|