@pippit-dev/cli 1.0.14 → 1.0.16
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 +43 -0
- package/checksums.txt +6 -6
- package/cmd/generate_video/generate_video.go +5 -0
- package/cmd/generate_video_test.go +20 -0
- package/cmd/root.go +4 -1
- package/cmd/short_drama_test.go +3 -1
- package/cmd/video_tool/video_tool.go +63 -0
- package/cmd/video_tool_test.go +258 -0
- package/internal/common/media.go +23 -0
- package/internal/common/media_test.go +17 -0
- package/internal/common/submit_run.go +45 -1
- package/internal/common/submit_run_test.go +28 -0
- package/internal/common/video_part.go +44 -0
- package/internal/generate_video/generate_video.go +31 -96
- package/internal/generate_video/generate_video_test.go +17 -0
- package/internal/video_tool/video_tool.go +130 -0
- package/internal/video_tool/video_tool_test.go +54 -0
- package/package.json +1 -1
- package/skills/xyq-nest-skill/SKILL.md +78 -18
package/README.md
CHANGED
|
@@ -273,6 +273,49 @@ pippit-tool-cli generate-video \
|
|
|
273
273
|
|
|
274
274
|
命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP 请求时,参考图、参考视频和参考音频会使用上传接口返回的 `pippit_asset_id`,并分别写入 `video_part_tool_param.images`、`video_part_tool_param.videos` 和 `video_part_tool_param.audios`。图片最多 9 张,支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频最多 3 个,支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频最多 3 个,仅支持 `.mp3`、`.wav`。普通用户支持模型 `Seedance_2.0_mini_lite`;`seedance2.0_vision`、`seedance2.0_fast_vision` 和 `Seedance_2.0_mini` 为 VIP 专属模型。CLI 会在提交前校验 prompt、素材数量和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
|
|
275
275
|
|
|
276
|
+
首尾帧生视频时,按首帧、尾帧的顺序传入两次 `--image`,并设置 `--generate-type 1`:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
pippit-tool-cli generate-video \
|
|
280
|
+
--prompt "让镜头从首帧平滑过渡到尾帧" \
|
|
281
|
+
--image "~/images/first.jpg" \
|
|
282
|
+
--image "~/images/last.jpg" \
|
|
283
|
+
--duration 5 \
|
|
284
|
+
--ratio "16:9" \
|
|
285
|
+
--model "Seedance_2.0_mini" \
|
|
286
|
+
--resolution "720p" \
|
|
287
|
+
--generate-type 1
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`--generate-type` 可选,填写后原样写入 `video_part_tool_param.generate_type`;值 `1` 表示首尾帧生成。CLI 保持图片上传和请求中的输入顺序,不在本地校验该参数的枚举值,具体能力与约束由服务端决定。
|
|
291
|
+
|
|
292
|
+
## 视频处理工具 CLI
|
|
293
|
+
|
|
294
|
+
`video-super-resolution` 会上传一个本地视频并提交视频超分任务:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
pippit-tool-cli video-super-resolution \
|
|
298
|
+
--video "~/videos/source.mp4" \
|
|
299
|
+
--output-resolution "1080p" \
|
|
300
|
+
--tool-version "standard"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`--output-resolution` 必填,当前服务端支持 `720p`、`1080p`、`2k`、`4k`。`--tool-version` 可选,当前服务端支持 `standard`、`professional_v1`、`professional_v2`;省略时由服务端使用 `standard`。CLI 不重复校验这些枚举值,具体能力与约束由服务端决定。
|
|
304
|
+
|
|
305
|
+
`erase-video-subtitle` 会上传一个本地视频并提交擦字幕任务:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
pippit-tool-cli erase-video-subtitle \
|
|
309
|
+
--video "~/videos/with-subtitle.mp4"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
两个命令都会把 `agent_name` 固定为 `pippit_video_part_agent`。上传接口返回的 `pippit_asset_id` 不会写入顶层 `asset_ids` 或普通参考视频列表,而是分别写入以下服务端专属参数:
|
|
313
|
+
|
|
314
|
+
- 超分:`video_part_tool_param.mini_tool_param.tool_param.video_super_resolution_tool_param.video.pippit_asset_id`
|
|
315
|
+
- 擦字幕:`video_part_tool_param.mini_tool_param.tool_param.erase_video_subtitle_tool_param.video.pippit_asset_id`
|
|
316
|
+
|
|
317
|
+
两个命令都输出 `thread_id`、`run_id` 和 `web_thread_link`。拿到任务 ID 后,可继续使用 `query-result` 查询并下载结果。
|
|
318
|
+
|
|
276
319
|
查询并下载生图/生视频结果:
|
|
277
320
|
|
|
278
321
|
```bash
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
74b2f14a1d1c02ad9494f673f6a89aa4d04ce7b044858b844ec023751f9e9cdd pippit-tool-cli-1.0.16-darwin-amd64.tar.gz
|
|
2
|
+
d84cb1159556082fb2e8eac85b6057a486dc2da35e3799f78f7a3bf80f3189e4 pippit-tool-cli-1.0.16-darwin-arm64.tar.gz
|
|
3
|
+
e7fc7e01d8a3a3a30263ba335d89effd294b8e4c965664994a6177fb6ef95c9f pippit-tool-cli-1.0.16-linux-amd64.tar.gz
|
|
4
|
+
197d60edff7e28ca0c8125c08f6bdd9b77844b4f7b6016207de8212e53300cfe pippit-tool-cli-1.0.16-linux-arm64.tar.gz
|
|
5
|
+
9895851ce36cd390c5841bf9ba3098e5f71a0b48f0cfb628e51a64b695dd692d pippit-tool-cli-1.0.16-windows-amd64.zip
|
|
6
|
+
c55f1d861640f8e6a5a5efa0d2cc30888658c47aaa09c30e1c94cc8fc4173a1d pippit-tool-cli-1.0.16-windows-arm64.zip
|
|
@@ -13,6 +13,7 @@ import (
|
|
|
13
13
|
func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
14
14
|
opts := &internalgen.Options{}
|
|
15
15
|
var durationSec int
|
|
16
|
+
var generateType int64
|
|
16
17
|
|
|
17
18
|
cmd := &cobra.Command{
|
|
18
19
|
Use: "generate-video",
|
|
@@ -22,6 +23,9 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
22
23
|
if cmd.Flags().Changed("duration") {
|
|
23
24
|
opts.DurationSec = &durationSec
|
|
24
25
|
}
|
|
26
|
+
if cmd.Flags().Changed("generate-type") {
|
|
27
|
+
opts.GenerateType = &generateType
|
|
28
|
+
}
|
|
25
29
|
|
|
26
30
|
result, err := internalgen.Run(cmd.Context(), opts, runner)
|
|
27
31
|
if err != nil {
|
|
@@ -46,5 +50,6 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
46
50
|
flags.StringVar(&opts.Ratio, "ratio", "", "video ratio, such as 9:16, 16:9, 3:4, 4:3")
|
|
47
51
|
flags.StringVar(&opts.Model, "model", "", "video model; normal users: Seedance_2.0_mini_lite; VIP-only: seedance2.0_vision, seedance2.0_fast_vision, Seedance_2.0_mini")
|
|
48
52
|
flags.StringVar(&opts.Resolution, "resolution", "", "video resolution, such as 720p, 1080p")
|
|
53
|
+
flags.Int64Var(&generateType, "generate-type", 0, "generation type passed to the service; set 1 for first-and-last-frame generation and provide two --image values in first-frame, last-frame order")
|
|
49
54
|
return cmd
|
|
50
55
|
}
|
|
@@ -72,6 +72,9 @@ func TestGenerateVideo(t *testing.T) {
|
|
|
72
72
|
if param["ratio"] != "9:16" || param["model"] != "seedance2.0_vision" || param["resolution"] != "720p" {
|
|
73
73
|
t.Fatalf("param = %#v, want ratio/model/resolution", param)
|
|
74
74
|
}
|
|
75
|
+
if param["generate_type"] != float64(1) {
|
|
76
|
+
t.Fatalf("generate_type = %v, want 1", param["generate_type"])
|
|
77
|
+
}
|
|
75
78
|
assertAssetRefs(t, param["images"], []string{"image_asset_1", "image_asset_2"})
|
|
76
79
|
assertAssetRefs(t, param["videos"], []string{"video_asset_1", "video_asset_2"})
|
|
77
80
|
assertAssetRefs(t, param["audios"], []string{"audio_asset_1"})
|
|
@@ -108,6 +111,7 @@ func TestGenerateVideo(t *testing.T) {
|
|
|
108
111
|
"--ratio", "9:16",
|
|
109
112
|
"--model", "seedance2.0_vision",
|
|
110
113
|
"--resolution", "720p",
|
|
114
|
+
"--generate-type", "1",
|
|
111
115
|
})
|
|
112
116
|
|
|
113
117
|
if err := root.Execute(); err != nil {
|
|
@@ -124,6 +128,21 @@ func TestGenerateVideoSkipsSemanticValidation(t *testing.T) {
|
|
|
124
128
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
125
129
|
switch r.URL.Path {
|
|
126
130
|
case "/api/biz/v1/skill/submit_run":
|
|
131
|
+
data, err := io.ReadAll(r.Body)
|
|
132
|
+
if err != nil {
|
|
133
|
+
t.Fatalf("read body: %v", err)
|
|
134
|
+
}
|
|
135
|
+
var body map[string]any
|
|
136
|
+
if err := sonic.Unmarshal(data, &body); err != nil {
|
|
137
|
+
t.Fatalf("decode body: %v", err)
|
|
138
|
+
}
|
|
139
|
+
param, ok := body["video_part_tool_param"].(map[string]any)
|
|
140
|
+
if !ok {
|
|
141
|
+
t.Fatalf("video_part_tool_param = %#v, want object", body["video_part_tool_param"])
|
|
142
|
+
}
|
|
143
|
+
if param["generate_type"] != float64(99) {
|
|
144
|
+
t.Fatalf("generate_type = %v, want 99", param["generate_type"])
|
|
145
|
+
}
|
|
127
146
|
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_123","run_id":"run_456"}}}`))
|
|
128
147
|
default:
|
|
129
148
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
@@ -140,6 +159,7 @@ func TestGenerateVideoSkipsSemanticValidation(t *testing.T) {
|
|
|
140
159
|
"--ratio", "1:1",
|
|
141
160
|
"--model", "bad_model",
|
|
142
161
|
"--resolution", "bad_resolution",
|
|
162
|
+
"--generate-type", "99",
|
|
143
163
|
})
|
|
144
164
|
|
|
145
165
|
if err := root.Execute(); err != nil {
|
package/cmd/root.go
CHANGED
|
@@ -11,6 +11,7 @@ import (
|
|
|
11
11
|
"github.com/Pippit-dev/pippit-cli/cmd/generate_video"
|
|
12
12
|
"github.com/Pippit-dev/pippit-cli/cmd/short_drama"
|
|
13
13
|
updatecmd "github.com/Pippit-dev/pippit-cli/cmd/update"
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/cmd/video_tool"
|
|
14
15
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
15
16
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
16
17
|
"github.com/Pippit-dev/pippit-cli/internal/version"
|
|
@@ -33,7 +34,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
33
34
|
root := &cobra.Command{
|
|
34
35
|
Use: "pippit-tool-cli",
|
|
35
36
|
Short: "Pippit CLI",
|
|
36
|
-
Long: "Pippit CLI generates videos and images, submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
37
|
+
Long: "Pippit CLI generates and processes videos and images, submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
37
38
|
Version: version.Current(),
|
|
38
39
|
SilenceUsage: true,
|
|
39
40
|
SilenceErrors: true,
|
|
@@ -49,6 +50,8 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
49
50
|
root.AddCommand(generate_image.NewCommand(stdout, stderr, runner))
|
|
50
51
|
root.AddCommand(generate_video.NewCommand(stdout, stderr, runner))
|
|
51
52
|
root.AddCommand(generate_video.NewQueryResultCommand(stdout, stderr, runner))
|
|
53
|
+
root.AddCommand(video_tool.NewSuperResolutionCommand(stdout, stderr, runner))
|
|
54
|
+
root.AddCommand(video_tool.NewEraseSubtitleCommand(stdout, stderr, runner))
|
|
52
55
|
root.AddCommand(short_drama.NewCommand(stdout, stderr, runner))
|
|
53
56
|
root.AddCommand(updatecmd.NewCommand(stdout, stderr))
|
|
54
57
|
localizeFlagErrors(root)
|
package/cmd/short_drama_test.go
CHANGED
|
@@ -124,7 +124,8 @@ func TestRootHelpListsSupportedCommands(t *testing.T) {
|
|
|
124
124
|
}
|
|
125
125
|
got := stdout.String()
|
|
126
126
|
for _, want := range []string{
|
|
127
|
-
"Pippit CLI generates videos",
|
|
127
|
+
"Pippit CLI generates and processes videos",
|
|
128
|
+
"erase-video-subtitle",
|
|
128
129
|
"generate-video",
|
|
129
130
|
"download-result",
|
|
130
131
|
"get-thread",
|
|
@@ -132,6 +133,7 @@ func TestRootHelpListsSupportedCommands(t *testing.T) {
|
|
|
132
133
|
"query-result",
|
|
133
134
|
"short-drama",
|
|
134
135
|
"update",
|
|
136
|
+
"video-super-resolution",
|
|
135
137
|
"--version",
|
|
136
138
|
} {
|
|
137
139
|
if !strings.Contains(got, want) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package video_tool
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"io"
|
|
5
|
+
|
|
6
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
7
|
+
internaltool "github.com/Pippit-dev/pippit-cli/internal/video_tool"
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// NewSuperResolutionCommand builds the video-super-resolution command.
|
|
12
|
+
func NewSuperResolutionCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
13
|
+
opts := &internaltool.SuperResolutionOptions{}
|
|
14
|
+
cmd := &cobra.Command{
|
|
15
|
+
Use: "video-super-resolution",
|
|
16
|
+
Short: "Improve the resolution of one video",
|
|
17
|
+
Args: cobra.NoArgs,
|
|
18
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
19
|
+
result, err := internaltool.RunSuperResolution(cmd.Context(), opts, runner)
|
|
20
|
+
if err != nil {
|
|
21
|
+
_ = common.AppendDailyErrorLog("video-super-resolution", err, map[string]string{
|
|
22
|
+
"video_path": opts.VideoPath,
|
|
23
|
+
"tool_version": opts.ToolVersion,
|
|
24
|
+
"output_resolution": opts.OutputResolution,
|
|
25
|
+
})
|
|
26
|
+
return err
|
|
27
|
+
}
|
|
28
|
+
return common.WriteJSON(stdout, result)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
cmd.SetOut(stdout)
|
|
32
|
+
cmd.SetErr(stderr)
|
|
33
|
+
flags := cmd.Flags()
|
|
34
|
+
flags.StringVar(&opts.VideoPath, "video", "", "local input video path")
|
|
35
|
+
flags.StringVar(&opts.OutputResolution, "output-resolution", "", "output resolution passed to the service: 720p, 1080p, 2k, or 4k")
|
|
36
|
+
flags.StringVar(&opts.ToolVersion, "tool-version", "", "optional tool version passed to the service: standard, professional_v1, or professional_v2")
|
|
37
|
+
return cmd
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// NewEraseSubtitleCommand builds the erase-video-subtitle command.
|
|
41
|
+
func NewEraseSubtitleCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
42
|
+
opts := &internaltool.EraseSubtitleOptions{}
|
|
43
|
+
cmd := &cobra.Command{
|
|
44
|
+
Use: "erase-video-subtitle",
|
|
45
|
+
Short: "Erase subtitles from one video",
|
|
46
|
+
Args: cobra.NoArgs,
|
|
47
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
48
|
+
result, err := internaltool.RunEraseSubtitle(cmd.Context(), opts, runner)
|
|
49
|
+
if err != nil {
|
|
50
|
+
_ = common.AppendDailyErrorLog("erase-video-subtitle", err, map[string]string{
|
|
51
|
+
"video_path": opts.VideoPath,
|
|
52
|
+
})
|
|
53
|
+
return err
|
|
54
|
+
}
|
|
55
|
+
return common.WriteJSON(stdout, result)
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
cmd.SetOut(stdout)
|
|
59
|
+
cmd.SetErr(stderr)
|
|
60
|
+
flags := cmd.Flags()
|
|
61
|
+
flags.StringVar(&opts.VideoPath, "video", "", "local input video path")
|
|
62
|
+
return cmd
|
|
63
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"io"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
|
|
13
|
+
"github.com/bytedance/sonic"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestVideoSuperResolution(t *testing.T) {
|
|
17
|
+
var uploaded bool
|
|
18
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
19
|
+
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
20
|
+
t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
|
|
21
|
+
}
|
|
22
|
+
switch r.URL.Path {
|
|
23
|
+
case "/api/biz/v1/skill/upload_file":
|
|
24
|
+
assertSingleUploadedVideo(t, r, "source.mp4")
|
|
25
|
+
uploaded = true
|
|
26
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"video_asset_1"}}`))
|
|
27
|
+
case "/api/biz/v1/skill/submit_run":
|
|
28
|
+
if !uploaded {
|
|
29
|
+
t.Fatal("submit called before upload")
|
|
30
|
+
}
|
|
31
|
+
body := decodeRequestBody(t, r)
|
|
32
|
+
assertVideoToolBaseRequest(t, body, "提升视频清晰度")
|
|
33
|
+
miniTool := videoMiniToolParam(t, body)
|
|
34
|
+
if miniTool["tool_name"] != "video_super_resolution" {
|
|
35
|
+
t.Fatalf("tool_name = %v, want video_super_resolution", miniTool["tool_name"])
|
|
36
|
+
}
|
|
37
|
+
toolParam := requiredObject(t, miniTool, "tool_param")
|
|
38
|
+
superResolution := requiredObject(t, toolParam, "video_super_resolution_tool_param")
|
|
39
|
+
if superResolution["tool_version"] != "professional_v1" {
|
|
40
|
+
t.Fatalf("tool_version = %v, want professional_v1", superResolution["tool_version"])
|
|
41
|
+
}
|
|
42
|
+
if superResolution["output_resolution"] != "2k" {
|
|
43
|
+
t.Fatalf("output_resolution = %v, want 2k", superResolution["output_resolution"])
|
|
44
|
+
}
|
|
45
|
+
assertNestedVideoAsset(t, superResolution, "video_asset_1")
|
|
46
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_sr","run_id":"run_sr"},"web_thread_link":"https://xyq.example/thread_sr"}}`))
|
|
47
|
+
default:
|
|
48
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
49
|
+
}
|
|
50
|
+
}))
|
|
51
|
+
defer server.Close()
|
|
52
|
+
|
|
53
|
+
video := writeTestVideo(t, "source.mp4")
|
|
54
|
+
var stdout, stderr bytes.Buffer
|
|
55
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
56
|
+
root.SetArgs([]string{
|
|
57
|
+
"video-super-resolution",
|
|
58
|
+
"--video", video,
|
|
59
|
+
"--output-resolution", "2k",
|
|
60
|
+
"--tool-version", "professional_v1",
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if err := root.Execute(); err != nil {
|
|
64
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
65
|
+
}
|
|
66
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
67
|
+
if got["thread_id"] != "thread_sr" || got["run_id"] != "run_sr" {
|
|
68
|
+
t.Fatalf("output = %#v, want super-resolution thread and run IDs", got)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func TestEraseVideoSubtitle(t *testing.T) {
|
|
73
|
+
var uploaded bool
|
|
74
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
75
|
+
switch r.URL.Path {
|
|
76
|
+
case "/api/biz/v1/skill/upload_file":
|
|
77
|
+
assertSingleUploadedVideo(t, r, "subtitle.mov")
|
|
78
|
+
uploaded = true
|
|
79
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"video_asset_2"}}`))
|
|
80
|
+
case "/api/biz/v1/skill/submit_run":
|
|
81
|
+
if !uploaded {
|
|
82
|
+
t.Fatal("submit called before upload")
|
|
83
|
+
}
|
|
84
|
+
body := decodeRequestBody(t, r)
|
|
85
|
+
assertVideoToolBaseRequest(t, body, "擦除视频字幕")
|
|
86
|
+
miniTool := videoMiniToolParam(t, body)
|
|
87
|
+
if miniTool["tool_name"] != "erase_video_subtitle" {
|
|
88
|
+
t.Fatalf("tool_name = %v, want erase_video_subtitle", miniTool["tool_name"])
|
|
89
|
+
}
|
|
90
|
+
toolParam := requiredObject(t, miniTool, "tool_param")
|
|
91
|
+
eraseSubtitle := requiredObject(t, toolParam, "erase_video_subtitle_tool_param")
|
|
92
|
+
if _, ok := eraseSubtitle["tool_version"]; ok {
|
|
93
|
+
t.Fatalf("tool_version must be omitted: %#v", eraseSubtitle)
|
|
94
|
+
}
|
|
95
|
+
assertNestedVideoAsset(t, eraseSubtitle, "video_asset_2")
|
|
96
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_erase","run_id":"run_erase"}}}`))
|
|
97
|
+
default:
|
|
98
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
99
|
+
}
|
|
100
|
+
}))
|
|
101
|
+
defer server.Close()
|
|
102
|
+
|
|
103
|
+
video := writeTestVideo(t, "subtitle.mov")
|
|
104
|
+
var stdout, stderr bytes.Buffer
|
|
105
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
106
|
+
root.SetArgs([]string{
|
|
107
|
+
"erase-video-subtitle",
|
|
108
|
+
"--video", video,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if err := root.Execute(); err != nil {
|
|
112
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
113
|
+
}
|
|
114
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
115
|
+
if got["thread_id"] != "thread_erase" || got["run_id"] != "run_erase" {
|
|
116
|
+
t.Fatalf("output = %#v, want erase-subtitle thread and run IDs", got)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func TestEraseVideoSubtitleDoesNotExposeToolVersion(t *testing.T) {
|
|
121
|
+
var stdout, stderr bytes.Buffer
|
|
122
|
+
root := newTestRootCommand(t, &stdout, &stderr, "http://127.0.0.1")
|
|
123
|
+
root.SetArgs([]string{
|
|
124
|
+
"erase-video-subtitle",
|
|
125
|
+
"--video", "source.mp4",
|
|
126
|
+
"--tool-version", "custom_version",
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
err := root.Execute()
|
|
130
|
+
if err == nil || !strings.Contains(err.Error(), "未知参数: --tool-version") {
|
|
131
|
+
t.Fatalf("Execute() error = %v, want unknown --tool-version", err)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func TestVideoToolErrorLogsIncludeVideoPath(t *testing.T) {
|
|
136
|
+
tests := []struct {
|
|
137
|
+
name string
|
|
138
|
+
args func(string) []string
|
|
139
|
+
}{
|
|
140
|
+
{
|
|
141
|
+
name: "super resolution",
|
|
142
|
+
args: func(path string) []string {
|
|
143
|
+
return []string{"video-super-resolution", "--video", path, "--output-resolution", "1080p"}
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "erase subtitle",
|
|
148
|
+
args: func(path string) []string {
|
|
149
|
+
return []string{"erase-video-subtitle", "--video", path}
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for _, tt := range tests {
|
|
155
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
156
|
+
clearDailyErrorLog(t)
|
|
157
|
+
videoPath := filepath.Join(t.TempDir(), "missing.mp4")
|
|
158
|
+
var stdout, stderr bytes.Buffer
|
|
159
|
+
root := newTestRootCommand(t, &stdout, &stderr, "http://127.0.0.1")
|
|
160
|
+
root.SetArgs(tt.args(videoPath))
|
|
161
|
+
|
|
162
|
+
if err := root.Execute(); err == nil {
|
|
163
|
+
t.Fatal("Execute() error = nil, want missing video error")
|
|
164
|
+
}
|
|
165
|
+
entries := readDailyErrorLog(t)
|
|
166
|
+
if len(entries) != 1 {
|
|
167
|
+
t.Fatalf("log entries = %d, want 1: %#v", len(entries), entries)
|
|
168
|
+
}
|
|
169
|
+
fields, ok := entries[0]["fields"].(map[string]any)
|
|
170
|
+
if !ok {
|
|
171
|
+
t.Fatalf("fields = %#v, want object", entries[0]["fields"])
|
|
172
|
+
}
|
|
173
|
+
if fields["video_path"] != videoPath {
|
|
174
|
+
t.Fatalf("video_path = %v, want %s", fields["video_path"], videoPath)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func assertSingleUploadedVideo(t *testing.T, r *http.Request, wantName string) {
|
|
181
|
+
t.Helper()
|
|
182
|
+
if r.Method != http.MethodPost {
|
|
183
|
+
t.Fatalf("upload method = %s, want POST", r.Method)
|
|
184
|
+
}
|
|
185
|
+
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
186
|
+
t.Fatalf("ParseMultipartForm(): %v", err)
|
|
187
|
+
}
|
|
188
|
+
files := r.MultipartForm.File["file"]
|
|
189
|
+
if len(files) != 1 || files[0].Filename != wantName {
|
|
190
|
+
t.Fatalf("uploaded files = %#v, want one %s", files, wantName)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func decodeRequestBody(t *testing.T, r *http.Request) map[string]any {
|
|
195
|
+
t.Helper()
|
|
196
|
+
data, err := io.ReadAll(r.Body)
|
|
197
|
+
if err != nil {
|
|
198
|
+
t.Fatalf("read body: %v", err)
|
|
199
|
+
}
|
|
200
|
+
var body map[string]any
|
|
201
|
+
if err := sonic.Unmarshal(data, &body); err != nil {
|
|
202
|
+
t.Fatalf("decode body: %v", err)
|
|
203
|
+
}
|
|
204
|
+
return body
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func assertVideoToolBaseRequest(t *testing.T, body map[string]any, wantMessage string) {
|
|
208
|
+
t.Helper()
|
|
209
|
+
if body["agent_name"] != "pippit_video_part_agent" {
|
|
210
|
+
t.Fatalf("agent_name = %v, want video part agent", body["agent_name"])
|
|
211
|
+
}
|
|
212
|
+
if body["message"] != wantMessage {
|
|
213
|
+
t.Fatalf("message = %v, want %s", body["message"], wantMessage)
|
|
214
|
+
}
|
|
215
|
+
if _, ok := body["asset_ids"]; ok {
|
|
216
|
+
t.Fatalf("asset_ids must be omitted: %#v", body)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func videoMiniToolParam(t *testing.T, body map[string]any) map[string]any {
|
|
221
|
+
t.Helper()
|
|
222
|
+
videoPart := requiredObject(t, body, "video_part_tool_param")
|
|
223
|
+
for _, unwanted := range []string{"prompt", "videos", "model", "ratio", "resolution", "duration_sec"} {
|
|
224
|
+
if _, ok := videoPart[unwanted]; ok {
|
|
225
|
+
t.Fatalf("video_part_tool_param.%s must be omitted: %#v", unwanted, videoPart)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return requiredObject(t, videoPart, "mini_tool_param")
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func assertNestedVideoAsset(t *testing.T, toolParam map[string]any, wantAssetID string) {
|
|
232
|
+
t.Helper()
|
|
233
|
+
video := requiredObject(t, toolParam, "video")
|
|
234
|
+
if video["pippit_asset_id"] != wantAssetID {
|
|
235
|
+
t.Fatalf("pippit_asset_id = %v, want %s", video["pippit_asset_id"], wantAssetID)
|
|
236
|
+
}
|
|
237
|
+
if len(video) != 1 {
|
|
238
|
+
t.Fatalf("video = %#v, want only pippit_asset_id", video)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func requiredObject(t *testing.T, parent map[string]any, key string) map[string]any {
|
|
243
|
+
t.Helper()
|
|
244
|
+
value, ok := parent[key].(map[string]any)
|
|
245
|
+
if !ok {
|
|
246
|
+
t.Fatalf("%s = %#v, want object", key, parent[key])
|
|
247
|
+
}
|
|
248
|
+
return value
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func writeTestVideo(t *testing.T, name string) string {
|
|
252
|
+
t.Helper()
|
|
253
|
+
path := filepath.Join(t.TempDir(), name)
|
|
254
|
+
if err := os.WriteFile(path, []byte("video-data"), 0o644); err != nil {
|
|
255
|
+
t.Fatalf("WriteFile(%s): %v", path, err)
|
|
256
|
+
}
|
|
257
|
+
return path
|
|
258
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
var (
|
|
10
|
+
allowedVideoExtensionList = []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v"}
|
|
11
|
+
allowedVideoExtensions = StringSet(allowedVideoExtensionList)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// ValidateVideoExtensions validates local video paths against the CLI-supported suffixes.
|
|
15
|
+
func ValidateVideoExtensions(paths []string) error {
|
|
16
|
+
for _, path := range paths {
|
|
17
|
+
ext := strings.ToLower(filepath.Ext(strings.TrimSpace(path)))
|
|
18
|
+
if _, ok := allowedVideoExtensions[ext]; !ok {
|
|
19
|
+
return fmt.Errorf("不支持的视频文件后缀 %q,文件:%q;支持的后缀:%s", ext, path, strings.Join(allowedVideoExtensionList, ", "))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return nil
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestValidateVideoExtensions(t *testing.T) {
|
|
9
|
+
if err := ValidateVideoExtensions([]string{"source.MP4", "reference.webm"}); err != nil {
|
|
10
|
+
t.Fatalf("ValidateVideoExtensions() error = %v, want nil", err)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
err := ValidateVideoExtensions([]string{"source.txt"})
|
|
14
|
+
if err == nil || !strings.Contains(err.Error(), `不支持的视频文件后缀 ".txt"`) {
|
|
15
|
+
t.Fatalf("ValidateVideoExtensions() error = %v, want extension validation", err)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
package common
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// AgentNameVideoPart is the video-part agent used by direct video capabilities.
|
|
11
|
+
const AgentNameVideoPart = "pippit_video_part_agent"
|
|
12
|
+
|
|
13
|
+
// SubmitRunResult is the shared successful submit_run output.
|
|
14
|
+
type SubmitRunResult struct {
|
|
15
|
+
ThreadID string `json:"thread_id"`
|
|
16
|
+
RunID string `json:"run_id"`
|
|
17
|
+
WebThreadLink string `json:"web_thread_link"`
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
// SubmitRunResponse is the shared response envelope returned by submit_run.
|
|
6
21
|
type SubmitRunResponse struct {
|
|
@@ -20,6 +35,35 @@ type SubmitRunResponseRun struct {
|
|
|
20
35
|
RunID string `json:"run_id"`
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
// SubmitRun sends a submit_run request and validates its shared response envelope.
|
|
39
|
+
func SubmitRun(ctx context.Context, command string, body any, runner *Runner) (*SubmitRunResult, error) {
|
|
40
|
+
if runner == nil || runner.Client == nil {
|
|
41
|
+
return nil, fmt.Errorf("%s 运行器客户端缺失", command)
|
|
42
|
+
}
|
|
43
|
+
var resp SubmitRunResponse
|
|
44
|
+
if err := runner.Client.SendRequest(ctx, SubmitRunPath(runner), body, &resp); err != nil {
|
|
45
|
+
return nil, fmt.Errorf("提交 %s 请求失败: %w", command, err)
|
|
46
|
+
}
|
|
47
|
+
if resp.Ret != "0" {
|
|
48
|
+
if resp.Errmsg == "" {
|
|
49
|
+
resp.Errmsg = "未知错误"
|
|
50
|
+
}
|
|
51
|
+
return nil, NewLogIDError(fmt.Sprintf("%s 请求返回失败: ret=%s errmsg=%s", command, resp.Ret, resp.Errmsg), resp.LogID)
|
|
52
|
+
}
|
|
53
|
+
if resp.Data.Run.ThreadID == "" {
|
|
54
|
+
return nil, fmt.Errorf("%s 响应缺少 data.run.thread_id", command)
|
|
55
|
+
}
|
|
56
|
+
if resp.Data.Run.RunID == "" {
|
|
57
|
+
return nil, fmt.Errorf("%s 响应缺少 data.run.run_id", command)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return &SubmitRunResult{
|
|
61
|
+
ThreadID: resp.Data.Run.ThreadID,
|
|
62
|
+
RunID: resp.Data.Run.RunID,
|
|
63
|
+
WebThreadLink: resp.Data.WebThreadLink,
|
|
64
|
+
}, nil
|
|
65
|
+
}
|
|
66
|
+
|
|
23
67
|
// SubmitRunPath returns the configured submit_run endpoint path.
|
|
24
68
|
func SubmitRunPath(runner *Runner) string {
|
|
25
69
|
if runner != nil && runner.Config != nil && runner.Config.Paths != nil && runner.Config.Paths.SubmitRun != "" {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestSubmitRunReturnsSharedResult(t *testing.T) {
|
|
10
|
+
result, err := SubmitRun(context.Background(), "generate-video", map[string]any{"message": "test"}, &Runner{
|
|
11
|
+
Client: getThreadFakeClient{response: `{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_123","run_id":"run_456"},"web_thread_link":"https://example.com/thread_123"}}`},
|
|
12
|
+
})
|
|
13
|
+
if err != nil {
|
|
14
|
+
t.Fatalf("SubmitRun() error = %v", err)
|
|
15
|
+
}
|
|
16
|
+
if result.ThreadID != "thread_123" || result.RunID != "run_456" || result.WebThreadLink != "https://example.com/thread_123" {
|
|
17
|
+
t.Fatalf("SubmitRun() result = %#v", result)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func TestSubmitRunBusinessErrorIncludesLogID(t *testing.T) {
|
|
22
|
+
_, err := SubmitRun(context.Background(), "video-super-resolution", nil, &Runner{
|
|
23
|
+
Client: getThreadFakeClient{response: `{"ret":"16008","errmsg":"提交Run任务失败","log_id":"log_123"}`},
|
|
24
|
+
})
|
|
25
|
+
if err == nil || !strings.Contains(err.Error(), "video-super-resolution 请求返回失败") || !strings.Contains(err.Error(), "log_id=log_123") {
|
|
26
|
+
t.Fatalf("SubmitRun() error = %v, want command and log_id", err)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
// MediaAsset references one uploaded Pippit media asset in submit_run requests.
|
|
4
|
+
type MediaAsset struct {
|
|
5
|
+
PippitAssetID string `json:"pippit_asset_id"`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// VideoPartToolParam is the shared request payload for video-part capabilities.
|
|
9
|
+
type VideoPartToolParam struct {
|
|
10
|
+
Images []*MediaAsset `json:"images,omitempty"`
|
|
11
|
+
Prompt *string `json:"prompt,omitempty"`
|
|
12
|
+
DurationSec *int `json:"duration_sec,omitempty"`
|
|
13
|
+
Ratio string `json:"ratio,omitempty"`
|
|
14
|
+
Videos []*MediaAsset `json:"videos,omitempty"`
|
|
15
|
+
Audios []*MediaAsset `json:"audios,omitempty"`
|
|
16
|
+
Model string `json:"model,omitempty"`
|
|
17
|
+
Resolution string `json:"resolution,omitempty"`
|
|
18
|
+
GenerateType *int64 `json:"generate_type,omitempty"`
|
|
19
|
+
MiniToolParam *VideoMiniToolParam `json:"mini_tool_param,omitempty"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// VideoMiniToolParam identifies a video mini tool and carries its request payload.
|
|
23
|
+
type VideoMiniToolParam struct {
|
|
24
|
+
ToolName string `json:"tool_name"`
|
|
25
|
+
ToolParam *VideoMiniToolPayload `json:"tool_param"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// VideoMiniToolPayload contains the request payload for one selected video mini tool.
|
|
29
|
+
type VideoMiniToolPayload struct {
|
|
30
|
+
VideoSuperResolutionToolParam *VideoSuperResolutionToolParam `json:"video_super_resolution_tool_param,omitempty"`
|
|
31
|
+
EraseVideoSubtitleToolParam *EraseVideoSubtitleToolParam `json:"erase_video_subtitle_tool_param,omitempty"`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// VideoSuperResolutionToolParam is the request payload for video super-resolution.
|
|
35
|
+
type VideoSuperResolutionToolParam struct {
|
|
36
|
+
ToolVersion string `json:"tool_version,omitempty"`
|
|
37
|
+
Video *MediaAsset `json:"video"`
|
|
38
|
+
OutputResolution string `json:"output_resolution"`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// EraseVideoSubtitleToolParam is the request payload for subtitle removal.
|
|
42
|
+
type EraseVideoSubtitleToolParam struct {
|
|
43
|
+
Video *MediaAsset `json:"video"`
|
|
44
|
+
}
|
|
@@ -3,7 +3,6 @@ package generate_video
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
|
-
"os"
|
|
7
6
|
"path/filepath"
|
|
8
7
|
"strings"
|
|
9
8
|
|
|
@@ -11,7 +10,6 @@ import (
|
|
|
11
10
|
)
|
|
12
11
|
|
|
13
12
|
const (
|
|
14
|
-
agentNameVideoPart = "pippit_video_part_agent"
|
|
15
13
|
maxReferenceImages = 9
|
|
16
14
|
maxReferenceVideos = 3
|
|
17
15
|
maxReferenceAudios = 3
|
|
@@ -19,46 +17,26 @@ const (
|
|
|
19
17
|
|
|
20
18
|
var (
|
|
21
19
|
allowedImageExtensionList = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"}
|
|
22
|
-
allowedVideoExtensionList = []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v"}
|
|
23
20
|
allowedAudioExtensionList = []string{".mp3", ".wav"}
|
|
24
|
-
allowedImageExtensions =
|
|
25
|
-
|
|
26
|
-
allowedAudioExtensions = makeExtensionSet(allowedAudioExtensionList)
|
|
21
|
+
allowedImageExtensions = common.StringSet(allowedImageExtensionList)
|
|
22
|
+
allowedAudioExtensions = common.StringSet(allowedAudioExtensionList)
|
|
27
23
|
)
|
|
28
24
|
|
|
29
25
|
// Options is the stable command-facing request shape for generate-video.
|
|
30
26
|
type Options struct {
|
|
31
|
-
Prompt
|
|
32
|
-
ImagePaths
|
|
33
|
-
VideoPaths
|
|
34
|
-
AudioPaths
|
|
35
|
-
DurationSec
|
|
36
|
-
Ratio
|
|
37
|
-
Model
|
|
38
|
-
Resolution
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type mediaAsset struct {
|
|
42
|
-
PippitAssetID string `json:"pippit_asset_id"`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type videoPartToolParam struct {
|
|
46
|
-
Images []mediaAsset `json:"images,omitempty"`
|
|
47
|
-
Prompt string `json:"prompt"`
|
|
48
|
-
DurationSec *int `json:"duration_sec,omitempty"`
|
|
49
|
-
Ratio string `json:"ratio,omitempty"`
|
|
50
|
-
Videos []mediaAsset `json:"videos,omitempty"`
|
|
51
|
-
Audios []mediaAsset `json:"audios,omitempty"`
|
|
52
|
-
Model string `json:"model,omitempty"`
|
|
53
|
-
Resolution string `json:"resolution,omitempty"`
|
|
27
|
+
Prompt string
|
|
28
|
+
ImagePaths []string
|
|
29
|
+
VideoPaths []string
|
|
30
|
+
AudioPaths []string
|
|
31
|
+
DurationSec *int
|
|
32
|
+
Ratio string
|
|
33
|
+
Model string
|
|
34
|
+
Resolution string
|
|
35
|
+
GenerateType *int64
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
// Result is the JSON envelope printed by `pippit-tool-cli generate-video`.
|
|
57
|
-
type Result
|
|
58
|
-
ThreadID string `json:"thread_id"`
|
|
59
|
-
RunID string `json:"run_id"`
|
|
60
|
-
WebThreadLink string `json:"web_thread_link"`
|
|
61
|
-
}
|
|
39
|
+
type Result = common.SubmitRunResult
|
|
62
40
|
|
|
63
41
|
func Run(ctx context.Context, opts *Options, runner *common.Runner) (*Result, error) {
|
|
64
42
|
if runner == nil || runner.Client == nil {
|
|
@@ -82,29 +60,7 @@ func Run(ctx context.Context, opts *Options, runner *common.Runner) (*Result, er
|
|
|
82
60
|
}
|
|
83
61
|
|
|
84
62
|
body := buildSubmitRunBody(opts, imageAssetIDs, videoAssetIDs, audioAssetIDs)
|
|
85
|
-
|
|
86
|
-
var resp common.SubmitRunResponse
|
|
87
|
-
if err := runner.Client.SendRequest(ctx, common.SubmitRunPath(runner), body, &resp); err != nil {
|
|
88
|
-
return nil, fmt.Errorf("提交 generate-video 请求失败: %w", err)
|
|
89
|
-
}
|
|
90
|
-
if resp.Ret != "0" {
|
|
91
|
-
if resp.Errmsg == "" {
|
|
92
|
-
resp.Errmsg = "未知错误"
|
|
93
|
-
}
|
|
94
|
-
return nil, common.NewLogIDError(fmt.Sprintf("generate-video 请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
|
|
95
|
-
}
|
|
96
|
-
if resp.Data.Run.ThreadID == "" {
|
|
97
|
-
return nil, fmt.Errorf("generate-video 响应缺少 data.run.thread_id")
|
|
98
|
-
}
|
|
99
|
-
if resp.Data.Run.RunID == "" {
|
|
100
|
-
return nil, fmt.Errorf("generate-video 响应缺少 data.run.run_id")
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return &Result{
|
|
104
|
-
ThreadID: resp.Data.Run.ThreadID,
|
|
105
|
-
RunID: resp.Data.Run.RunID,
|
|
106
|
-
WebThreadLink: resp.Data.WebThreadLink,
|
|
107
|
-
}, nil
|
|
63
|
+
return common.SubmitRun(ctx, "generate-video", body, runner)
|
|
108
64
|
}
|
|
109
65
|
|
|
110
66
|
func ValidateOptions(opts *Options) error {
|
|
@@ -126,7 +82,7 @@ func ValidateOptions(opts *Options) error {
|
|
|
126
82
|
if err := validateMediaExtensions("图片", opts.ImagePaths, allowedImageExtensions, allowedImageExtensionList); err != nil {
|
|
127
83
|
return err
|
|
128
84
|
}
|
|
129
|
-
if err :=
|
|
85
|
+
if err := common.ValidateVideoExtensions(opts.VideoPaths); err != nil {
|
|
130
86
|
return err
|
|
131
87
|
}
|
|
132
88
|
if err := validateMediaExtensions("音频", opts.AudioPaths, allowedAudioExtensions, allowedAudioExtensionList); err != nil {
|
|
@@ -145,18 +101,10 @@ func validateMediaExtensions(kind string, paths []string, allowed map[string]str
|
|
|
145
101
|
return nil
|
|
146
102
|
}
|
|
147
103
|
|
|
148
|
-
func makeExtensionSet(list []string) map[string]struct{} {
|
|
149
|
-
set := make(map[string]struct{}, len(list))
|
|
150
|
-
for _, ext := range list {
|
|
151
|
-
set[ext] = struct{}{}
|
|
152
|
-
}
|
|
153
|
-
return set
|
|
154
|
-
}
|
|
155
|
-
|
|
156
104
|
func uploadMediaList(ctx context.Context, paths []string, runner *common.Runner) ([]string, error) {
|
|
157
105
|
assetIDs := make([]string, 0, len(paths))
|
|
158
106
|
for _, path := range paths {
|
|
159
|
-
expanded, err :=
|
|
107
|
+
expanded, err := common.ExpandPath(path)
|
|
160
108
|
if err != nil {
|
|
161
109
|
return nil, err
|
|
162
110
|
}
|
|
@@ -170,46 +118,33 @@ func uploadMediaList(ctx context.Context, paths []string, runner *common.Runner)
|
|
|
170
118
|
}
|
|
171
119
|
|
|
172
120
|
func buildSubmitRunBody(opts *Options, imageAssetIDs []string, videoAssetIDs []string, audioAssetIDs []string) map[string]any {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
121
|
+
prompt := strings.TrimSpace(opts.Prompt)
|
|
122
|
+
param := common.VideoPartToolParam{
|
|
123
|
+
Images: assetRefs(imageAssetIDs),
|
|
124
|
+
Prompt: &prompt,
|
|
125
|
+
DurationSec: opts.DurationSec,
|
|
126
|
+
Ratio: strings.TrimSpace(opts.Ratio),
|
|
127
|
+
Videos: assetRefs(videoAssetIDs),
|
|
128
|
+
Audios: assetRefs(audioAssetIDs),
|
|
129
|
+
Model: strings.TrimSpace(opts.Model),
|
|
130
|
+
Resolution: strings.TrimSpace(opts.Resolution),
|
|
131
|
+
GenerateType: opts.GenerateType,
|
|
182
132
|
}
|
|
183
133
|
|
|
184
134
|
return map[string]any{
|
|
185
|
-
"agent_name":
|
|
186
|
-
"message":
|
|
135
|
+
"agent_name": common.AgentNameVideoPart,
|
|
136
|
+
"message": prompt,
|
|
187
137
|
"video_part_tool_param": param,
|
|
188
138
|
}
|
|
189
139
|
}
|
|
190
140
|
|
|
191
|
-
func assetRefs(assetIDs []string) []
|
|
141
|
+
func assetRefs(assetIDs []string) []*common.MediaAsset {
|
|
192
142
|
if len(assetIDs) == 0 {
|
|
193
143
|
return nil
|
|
194
144
|
}
|
|
195
|
-
refs := make([]
|
|
145
|
+
refs := make([]*common.MediaAsset, 0, len(assetIDs))
|
|
196
146
|
for _, assetID := range assetIDs {
|
|
197
|
-
refs = append(refs,
|
|
147
|
+
refs = append(refs, &common.MediaAsset{PippitAssetID: assetID})
|
|
198
148
|
}
|
|
199
149
|
return refs
|
|
200
150
|
}
|
|
201
|
-
|
|
202
|
-
func expandPath(path string) (string, error) {
|
|
203
|
-
path = strings.TrimSpace(path)
|
|
204
|
-
if path == "~" {
|
|
205
|
-
return os.UserHomeDir()
|
|
206
|
-
}
|
|
207
|
-
if strings.HasPrefix(path, "~/") || strings.HasPrefix(path, `~\`) {
|
|
208
|
-
home, err := os.UserHomeDir()
|
|
209
|
-
if err != nil {
|
|
210
|
-
return "", fmt.Errorf("解析用户主目录失败: %w", err)
|
|
211
|
-
}
|
|
212
|
-
return filepath.Join(home, path[2:]), nil
|
|
213
|
-
}
|
|
214
|
-
return path, nil
|
|
215
|
-
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package generate_video
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestBuildSubmitRunBodyPreservesEmptyPrompt(t *testing.T) {
|
|
9
|
+
body := buildSubmitRunBody(&Options{}, nil, nil, nil)
|
|
10
|
+
got, err := json.Marshal(body["video_part_tool_param"])
|
|
11
|
+
if err != nil {
|
|
12
|
+
t.Fatalf("json.Marshal() error = %v", err)
|
|
13
|
+
}
|
|
14
|
+
if string(got) != `{"prompt":""}` {
|
|
15
|
+
t.Fatalf("video_part_tool_param = %s, want explicit empty prompt", got)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package video_tool
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const (
|
|
12
|
+
toolVideoSuperResolution = "video_super_resolution"
|
|
13
|
+
toolEraseVideoSubtitle = "erase_video_subtitle"
|
|
14
|
+
messageSuperResolution = "提升视频清晰度"
|
|
15
|
+
messageEraseSubtitle = "擦除视频字幕"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// SuperResolutionOptions is the command-facing request shape for video-super-resolution.
|
|
19
|
+
type SuperResolutionOptions struct {
|
|
20
|
+
VideoPath string
|
|
21
|
+
ToolVersion string
|
|
22
|
+
OutputResolution string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// EraseSubtitleOptions is the command-facing request shape for erase-video-subtitle.
|
|
26
|
+
type EraseSubtitleOptions struct {
|
|
27
|
+
VideoPath string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Result is the JSON envelope printed by both video tool commands.
|
|
31
|
+
type Result = common.SubmitRunResult
|
|
32
|
+
|
|
33
|
+
// RunSuperResolution uploads one video and submits a video super-resolution run.
|
|
34
|
+
func RunSuperResolution(ctx context.Context, opts *SuperResolutionOptions, runner *common.Runner) (*Result, error) {
|
|
35
|
+
if runner == nil || runner.Client == nil {
|
|
36
|
+
return nil, fmt.Errorf("video-super-resolution 运行器客户端缺失")
|
|
37
|
+
}
|
|
38
|
+
if err := ValidateSuperResolutionOptions(opts); err != nil {
|
|
39
|
+
return nil, err
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
assetID, err := uploadVideo(ctx, opts.VideoPath, runner)
|
|
43
|
+
if err != nil {
|
|
44
|
+
return nil, err
|
|
45
|
+
}
|
|
46
|
+
return common.SubmitRun(ctx, "video-super-resolution", buildSuperResolutionSubmitRunBody(opts, assetID), runner)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// RunEraseSubtitle uploads one video and submits a subtitle-erasing run.
|
|
50
|
+
func RunEraseSubtitle(ctx context.Context, opts *EraseSubtitleOptions, runner *common.Runner) (*Result, error) {
|
|
51
|
+
if runner == nil || runner.Client == nil {
|
|
52
|
+
return nil, fmt.Errorf("erase-video-subtitle 运行器客户端缺失")
|
|
53
|
+
}
|
|
54
|
+
if err := ValidateEraseSubtitleOptions(opts); err != nil {
|
|
55
|
+
return nil, err
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assetID, err := uploadVideo(ctx, opts.VideoPath, runner)
|
|
59
|
+
if err != nil {
|
|
60
|
+
return nil, err
|
|
61
|
+
}
|
|
62
|
+
return common.SubmitRun(ctx, "erase-video-subtitle", buildEraseSubtitleSubmitRunBody(assetID), runner)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ValidateSuperResolutionOptions validates only command shape; semantic enums remain service-owned.
|
|
66
|
+
func ValidateSuperResolutionOptions(opts *SuperResolutionOptions) error {
|
|
67
|
+
if opts == nil || strings.TrimSpace(opts.VideoPath) == "" {
|
|
68
|
+
return fmt.Errorf("缺少必填参数 --video")
|
|
69
|
+
}
|
|
70
|
+
if strings.TrimSpace(opts.OutputResolution) == "" {
|
|
71
|
+
return fmt.Errorf("缺少必填参数 --output-resolution")
|
|
72
|
+
}
|
|
73
|
+
return common.ValidateVideoExtensions([]string{opts.VideoPath})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ValidateEraseSubtitleOptions validates the command shape.
|
|
77
|
+
func ValidateEraseSubtitleOptions(opts *EraseSubtitleOptions) error {
|
|
78
|
+
if opts == nil || strings.TrimSpace(opts.VideoPath) == "" {
|
|
79
|
+
return fmt.Errorf("缺少必填参数 --video")
|
|
80
|
+
}
|
|
81
|
+
return common.ValidateVideoExtensions([]string{opts.VideoPath})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func uploadVideo(ctx context.Context, path string, runner *common.Runner) (string, error) {
|
|
85
|
+
expanded, err := common.ExpandPath(path)
|
|
86
|
+
if err != nil {
|
|
87
|
+
return "", err
|
|
88
|
+
}
|
|
89
|
+
result, err := common.UploadFile(ctx, common.UploadFileOptions{Path: expanded}, runner)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return "", fmt.Errorf("上传视频失败: %w", err)
|
|
92
|
+
}
|
|
93
|
+
return result.AssetID, nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func buildSuperResolutionSubmitRunBody(opts *SuperResolutionOptions, assetID string) map[string]any {
|
|
97
|
+
return map[string]any{
|
|
98
|
+
"agent_name": common.AgentNameVideoPart,
|
|
99
|
+
"message": messageSuperResolution,
|
|
100
|
+
"video_part_tool_param": common.VideoPartToolParam{
|
|
101
|
+
MiniToolParam: &common.VideoMiniToolParam{
|
|
102
|
+
ToolName: toolVideoSuperResolution,
|
|
103
|
+
ToolParam: &common.VideoMiniToolPayload{
|
|
104
|
+
VideoSuperResolutionToolParam: &common.VideoSuperResolutionToolParam{
|
|
105
|
+
ToolVersion: strings.TrimSpace(opts.ToolVersion),
|
|
106
|
+
Video: &common.MediaAsset{PippitAssetID: assetID},
|
|
107
|
+
OutputResolution: strings.TrimSpace(opts.OutputResolution),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func buildEraseSubtitleSubmitRunBody(assetID string) map[string]any {
|
|
116
|
+
return map[string]any{
|
|
117
|
+
"agent_name": common.AgentNameVideoPart,
|
|
118
|
+
"message": messageEraseSubtitle,
|
|
119
|
+
"video_part_tool_param": common.VideoPartToolParam{
|
|
120
|
+
MiniToolParam: &common.VideoMiniToolParam{
|
|
121
|
+
ToolName: toolEraseVideoSubtitle,
|
|
122
|
+
ToolParam: &common.VideoMiniToolPayload{
|
|
123
|
+
EraseVideoSubtitleToolParam: &common.EraseVideoSubtitleToolParam{
|
|
124
|
+
Video: &common.MediaAsset{PippitAssetID: assetID},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package video_tool
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestValidateSuperResolutionOptions(t *testing.T) {
|
|
9
|
+
tests := []struct {
|
|
10
|
+
name string
|
|
11
|
+
opts *SuperResolutionOptions
|
|
12
|
+
wantContains string
|
|
13
|
+
}{
|
|
14
|
+
{name: "valid", opts: &SuperResolutionOptions{VideoPath: "source.mp4", OutputResolution: "1080p"}},
|
|
15
|
+
{name: "missing video", opts: &SuperResolutionOptions{OutputResolution: "1080p"}, wantContains: "--video"},
|
|
16
|
+
{name: "missing output resolution", opts: &SuperResolutionOptions{VideoPath: "source.mp4"}, wantContains: "--output-resolution"},
|
|
17
|
+
{name: "unsupported extension", opts: &SuperResolutionOptions{VideoPath: "source.txt", OutputResolution: "1080p"}, wantContains: "不支持的视频文件后缀"},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for _, tt := range tests {
|
|
21
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
22
|
+
err := ValidateSuperResolutionOptions(tt.opts)
|
|
23
|
+
if tt.wantContains == "" {
|
|
24
|
+
if err != nil {
|
|
25
|
+
t.Fatalf("ValidateSuperResolutionOptions() error = %v, want nil", err)
|
|
26
|
+
}
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if err == nil || !strings.Contains(err.Error(), tt.wantContains) {
|
|
30
|
+
t.Fatalf("ValidateSuperResolutionOptions() error = %v, want containing %q", err, tt.wantContains)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestValidateEraseSubtitleOptions(t *testing.T) {
|
|
37
|
+
if err := ValidateEraseSubtitleOptions(&EraseSubtitleOptions{VideoPath: "source.webm"}); err != nil {
|
|
38
|
+
t.Fatalf("ValidateEraseSubtitleOptions() error = %v, want nil", err)
|
|
39
|
+
}
|
|
40
|
+
err := ValidateEraseSubtitleOptions(&EraseSubtitleOptions{})
|
|
41
|
+
if err == nil || !strings.Contains(err.Error(), "--video") {
|
|
42
|
+
t.Fatalf("ValidateEraseSubtitleOptions() error = %v, want --video validation", err)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestSuperResolutionSemanticValuesRemainServiceOwned(t *testing.T) {
|
|
47
|
+
if err := ValidateSuperResolutionOptions(&SuperResolutionOptions{
|
|
48
|
+
VideoPath: "source.mp4",
|
|
49
|
+
ToolVersion: "future_version",
|
|
50
|
+
OutputResolution: "future_resolution",
|
|
51
|
+
}); err != nil {
|
|
52
|
+
t.Fatalf("ValidateSuperResolutionOptions() error = %v, want service-owned enum validation", err)
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xyq-skill
|
|
3
|
-
description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频,并在用户明确要求图片或视频模型直出、指定图片或视频模型或直接调用 CLI 时使用 pippit-tool-cli generate-image / generate-video
|
|
3
|
+
description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频,并在用户明确要求图片或视频模型直出、指定图片或视频模型或直接调用 CLI 时使用 pippit-tool-cli generate-image / generate-video;用户要求视频超分、提升视频清晰度、擦字幕或去字幕时,使用 video-super-resolution / erase-video-subtitle。覆盖文生图、文生视频、图生视频、首尾帧生视频、视频编辑、风格转换、视频续写、视频复刻、TVC、宣传片、音乐 MV、产品广告、分镜和教育短视频等场景。当用户提到小云雀、xyq、上传参考图/视频/mp3或wav音频、查看生成进度时也应触发。短剧生成、续写、改写、人物设定和分集创作应使用 xyq-short-drama-skill,不在本技能中执行。
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata:
|
|
6
6
|
{
|
|
@@ -17,7 +17,7 @@ metadata:
|
|
|
17
17
|
}
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# 小云雀创作、图片/视频模型直出与视频处理
|
|
21
21
|
|
|
22
22
|
通过 小云雀的API 创建会话、发送消息(生图、生视频、编辑视频等)、上传图片/视频/mp3或wav音频文件,并查询会话消息进展。
|
|
23
23
|
|
|
@@ -26,9 +26,10 @@ metadata:
|
|
|
26
26
|
**平台核心能力:**
|
|
27
27
|
- **生成**:文生图、文生视频、图生视频、视频续写
|
|
28
28
|
- **编辑**:局部修改、元素替换、镜头调整、风格迁移
|
|
29
|
+
- **视频处理**:视频超分、提升视频清晰度、擦字幕
|
|
29
30
|
- **复杂创作**:复刻已有视频风格做 TVC/宣传片、用音乐生成 MV、产品展示片制作
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
除“图片/视频模型直出”和“视频超分/擦字幕”外,创作和编辑需求通过发送自然语言消息来完成,后端 Agent 会自主编排工作流。复杂任务耗时较长,需耐心轮询。
|
|
32
33
|
|
|
33
34
|
## 执行路由(必须先判断)
|
|
34
35
|
|
|
@@ -71,6 +72,7 @@ pippit-tool-cli query-result \
|
|
|
71
72
|
|
|
72
73
|
- 用户明确说“视频模型直出”、“直接调模型”或“直接调用 CLI”。
|
|
73
74
|
- 用户指定了具体视频模型(如 Seedance),并希望单次直接生成视频。
|
|
75
|
+
- 用户明确要求“首尾帧生视频”、指定首帧和尾帧,或要求从第一张图过渡到第二张图。
|
|
74
76
|
- 上游流程已明确将任务标记为 direct-model / 模型直出。
|
|
75
77
|
|
|
76
78
|
执行原则:
|
|
@@ -78,13 +80,20 @@ pippit-tool-cli query-result \
|
|
|
78
80
|
1. 执行前用 `command -v pippit-tool-cli` 确认 CLI 可用;不可用时报告阻塞,不要悄悄降级到会话 API。
|
|
79
81
|
2. 真实提交会消耗 credits;如果用户本轮尚未明确确认生成,按“用户确认与反问”规则征得明确确认后再运行。
|
|
80
82
|
3. 保留用户原始 prompt,不要自行扩写、润色、翻译或增加风格词。
|
|
81
|
-
4. 只添加用户已经给出的 `--model`、`--duration`、`--ratio`、`--resolution`、`--image`、`--video`、`--audio` 参数;未给参数交给 CLI 默认值。
|
|
82
|
-
5.
|
|
83
|
-
6.
|
|
83
|
+
4. 只添加用户已经给出的 `--model`、`--duration`、`--ratio`、`--resolution`、`--image`、`--video`、`--audio`、`--generate-type` 参数;未给参数交给 CLI 默认值。
|
|
84
|
+
5. 首尾帧请求固定传 `--generate-type 1`,并按首帧、尾帧顺序传入两次 `--image`,不得重排。用户未明确两张图片的角色或缺少任一张时,先询问用户;不要在 skill 侧维护额外的 `generate_type` 枚举 allowlist,其他值原样交给服务端处理。
|
|
85
|
+
6. `generate-video` 返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。
|
|
86
|
+
7. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `videos[].output_path`。
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
89
|
pippit-tool-cli generate-video --prompt "用户原始描述"
|
|
87
90
|
|
|
91
|
+
pippit-tool-cli generate-video \
|
|
92
|
+
--prompt "用户原始描述" \
|
|
93
|
+
--image FIRST_FRAME_PATH \
|
|
94
|
+
--image LAST_FRAME_PATH \
|
|
95
|
+
--generate-type 1
|
|
96
|
+
|
|
88
97
|
pippit-tool-cli query-result \
|
|
89
98
|
--thread-id THREAD_ID \
|
|
90
99
|
--run-id RUN_ID \
|
|
@@ -93,11 +102,40 @@ pippit-tool-cli query-result \
|
|
|
93
102
|
|
|
94
103
|
`--image` 最多重复 9 次,`--video` 和 `--audio` 最多各重复 3 次。
|
|
95
104
|
|
|
96
|
-
### 路由 C
|
|
105
|
+
### 路由 C:视频超分和擦字幕
|
|
106
|
+
|
|
107
|
+
用户明确要求视频超分、提升视频清晰度、擦字幕或去字幕时,直接调用对应的 `pippit-tool-cli` 视频处理命令,不要改走 `submit_run.py`:
|
|
108
|
+
|
|
109
|
+
- 视频超分、提升视频清晰度:`video-super-resolution`
|
|
110
|
+
- 擦字幕、去字幕:`erase-video-subtitle`
|
|
111
|
+
|
|
112
|
+
执行原则:
|
|
113
|
+
|
|
114
|
+
1. 执行前用 `command -v pippit-tool-cli` 确认 CLI 可用;不可用时报告阻塞,不要悄悄降级到会话 API。
|
|
115
|
+
2. 真实提交会消耗 credits;如果用户本轮尚未明确确认处理,按“用户确认与反问”规则征得明确确认后再运行。
|
|
116
|
+
3. 把用户提供的本地视频路径和处理参数直接交给对应 CLI;缺少必填输入时先询问用户。
|
|
117
|
+
4. 命令返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。
|
|
118
|
+
5. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `videos[].output_path`。
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pippit-tool-cli video-super-resolution \
|
|
122
|
+
--video VIDEO_PATH \
|
|
123
|
+
--output-resolution OUTPUT_RESOLUTION
|
|
124
|
+
|
|
125
|
+
pippit-tool-cli erase-video-subtitle \
|
|
126
|
+
--video VIDEO_PATH
|
|
127
|
+
|
|
128
|
+
pippit-tool-cli query-result \
|
|
129
|
+
--thread-id THREAD_ID \
|
|
130
|
+
--run-id RUN_ID \
|
|
131
|
+
--download-dir OUTPUT_DIR
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 路由 D:小云雀后端 Agent 编排
|
|
97
135
|
|
|
98
|
-
需要意图确认、脚本/分镜拆解、MV、TVC、局部编辑、复杂参考素材编排,或者用户未明确要求模型直出时,继续使用本技能内置的 `submit_run.py` / `get_thread.py`
|
|
136
|
+
需要意图确认、脚本/分镜拆解、MV、TVC、局部编辑、复杂参考素材编排,或者用户未明确要求模型直出时,继续使用本技能内置的 `submit_run.py` / `get_thread.py` 会话工作流;明确的首尾帧请求走路由 B,明确的视频超分和擦字幕请求走路由 C。
|
|
99
137
|
|
|
100
|
-
### 路由
|
|
138
|
+
### 路由 E:短剧工作流
|
|
101
139
|
|
|
102
140
|
用户要求短剧生成、续写、改写、剧情扩展、人物设定、分集草稿或短剧会话文件处理时,停止本技能流程并转交 `xyq-short-drama-skill`,不要用 `submit_run.py`、`generate-image` 或 `generate-video` 假装执行完整短剧流程。
|
|
103
141
|
|
|
@@ -122,6 +160,7 @@ pippit-tool-cli query-result \
|
|
|
122
160
|
4. **下载结果** - 将会话中生成的图片/视频批量下载到本地,支持指定输出目录和文件名前缀。
|
|
123
161
|
5. **图片模型直出** - 使用 `pippit-tool-cli generate-image` 直接调用图片模型,使用 `query-result` 查询并下载图片结果。
|
|
124
162
|
6. **视频模型直出** - 使用 `pippit-tool-cli generate-video` 直接调用视频模型,使用 `query-result` 查询并下载视频结果。
|
|
163
|
+
7. **视频处理** - 使用 `pippit-tool-cli video-super-resolution` 或 `erase-video-subtitle` 处理本地视频,使用 `query-result` 查询并下载视频结果。
|
|
125
164
|
|
|
126
165
|
|
|
127
166
|
## 前置要求
|
|
@@ -132,7 +171,7 @@ export XYQ_ACCESS_KEY="your-access-key"
|
|
|
132
171
|
|
|
133
172
|
可选:`XYQ_OPENAPI_BASE` 或 `XYQ_BASE_URL`,默认 `https://xyq.jianying.com`。
|
|
134
173
|
|
|
135
|
-
会话 API 路由无需安装额外依赖,仅使用 Python
|
|
174
|
+
会话 API 路由无需安装额外依赖,仅使用 Python 标准库。图片/视频模型直出和视频处理路由额外要求 `pippit-tool-cli` 在 `PATH` 中可用。
|
|
136
175
|
|
|
137
176
|
## 使用方法
|
|
138
177
|
|
|
@@ -215,7 +254,18 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
215
254
|
6. completed=true 后展示并下载 images[].output_path;出现 error_message 时停止并报告
|
|
216
255
|
```
|
|
217
256
|
|
|
218
|
-
### 场景 3
|
|
257
|
+
### 场景 3:用户明确要求视频模型直出(含首尾帧)
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
1. command -v pippit-tool-cli → 确认 CLI 可用
|
|
261
|
+
2. 普通视频模型直出:pippit-tool-cli generate-video --prompt "用户原始描述" [仅添加用户已给出的其他参数]
|
|
262
|
+
3. 首尾帧直出:确认两张图片的首帧/尾帧角色,按顺序执行 generate-video --image FIRST_FRAME_PATH --image LAST_FRAME_PATH --generate-type 1
|
|
263
|
+
4. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link
|
|
264
|
+
5. 每隔 10 秒调用 query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir OUTPUT_DIR
|
|
265
|
+
6. completed=true 后展示并下载 videos[].output_path;出现 error_message 时停止并报告
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 场景 4:用户提供图片/视频/音频要求编辑修改或作为参考(如"参考这个视频做一个新的"、"用这首歌做MV")
|
|
219
269
|
|
|
220
270
|
```
|
|
221
271
|
1. upload_file.py /path/to/video.mp4 → 拿到 asset_id1
|
|
@@ -226,7 +276,7 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
226
276
|
|
|
227
277
|
用户给了文件路径 + 编辑指令 = 先上传文件,再把编辑指令和 所有asset_id 一起发送。
|
|
228
278
|
|
|
229
|
-
### 场景
|
|
279
|
+
### 场景 5:用户提供参考图/视频/音频要求生成新内容
|
|
230
280
|
|
|
231
281
|
```
|
|
232
282
|
1. upload_file.py /path/to/ref1.png → 拿到 asset_id1
|
|
@@ -237,13 +287,23 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
237
287
|
6. 后续同场景 1 的步骤 2-6
|
|
238
288
|
```
|
|
239
289
|
|
|
240
|
-
### 场景
|
|
290
|
+
### 场景 6:在已有会话中追加新需求
|
|
241
291
|
|
|
242
292
|
```
|
|
243
293
|
1. submit_run.py --message "新的描述" --thread-id THREAD_ID → 拿到 thread_id、run_id、web_thread_link
|
|
244
294
|
2. 后续同场景 1 的步骤 2-6
|
|
245
295
|
```
|
|
246
296
|
|
|
297
|
+
### 场景 7:用户要求视频超分或擦字幕
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
1. command -v pippit-tool-cli → 确认 CLI 可用
|
|
301
|
+
2. 根据用户意图调用 video-super-resolution 或 erase-video-subtitle,并传入用户提供的本地视频路径和处理参数
|
|
302
|
+
3. 拿到 thread_id、run_id 和 web_thread_link,立即展示 web_thread_link
|
|
303
|
+
4. 每隔 10 秒调用 query-result --thread-id THREAD_ID --run-id RUN_ID --download-dir OUTPUT_DIR
|
|
304
|
+
5. completed=true 后展示并下载 videos[].output_path;出现 error_message 时停止并报告
|
|
305
|
+
```
|
|
306
|
+
|
|
247
307
|
### 轮询策略
|
|
248
308
|
|
|
249
309
|
- **间隔**:每 10 秒查询一次
|
|
@@ -314,12 +374,12 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
314
374
|
|
|
315
375
|
## 核心原则:用户侧不做创作,只做传话
|
|
316
376
|
|
|
317
|
-
你(用户侧 Agent)的职责是**搬运工**,不是创作者。会话 API 路由由后端 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt
|
|
377
|
+
你(用户侧 Agent)的职责是**搬运工**,不是创作者。会话 API 路由由后端 Agent 负责理解需求、拆解分镜、编排工作流、选模型、写 prompt;图片/视频模型直出和视频处理路由把用户原始参数传给 CLI。你要做的是:
|
|
318
378
|
|
|
319
|
-
1. **准备素材**:会话 API 路由用 `upload_file.py` 把本地文件转为 asset_id
|
|
320
|
-
2. **提交任务**:先按“执行路由”判断;图片模型直出调用 `pippit-tool-cli generate-image`,视频模型直出调用 `pippit-tool-cli generate-video
|
|
379
|
+
1. **准备素材**:会话 API 路由用 `upload_file.py` 把本地文件转为 asset_id;图片/视频模型直出和视频处理路由把本地路径直接交给对应 CLI;首尾帧任务固定传 `--generate-type 1` 并保持首帧、尾帧顺序
|
|
380
|
+
2. **提交任务**:先按“执行路由”判断;图片模型直出调用 `pippit-tool-cli generate-image`,视频模型直出调用 `pippit-tool-cli generate-video`,视频超分和擦字幕调用对应的视频处理命令,其余任务把用户的原始描述 + asset_id 原封不动发给 `submit_run.py`
|
|
321
381
|
3. **传话**:根据 `get_thread.py` 返回的消息列表,展示过程中的意图询问、创作信息等
|
|
322
|
-
4. **取件**:会话 API 路由用 `get_thread.py`
|
|
382
|
+
4. **取件**:会话 API 路由用 `get_thread.py` 轮询,图片/视频模型直出和视频处理路由用 `query-result` 轮询 → 检查结果 → 下载产物 → 结果展示给用户
|
|
323
383
|
|
|
324
384
|
**绝对不要做的事:**
|
|
325
385
|
- 不要替用户扩写、润色、翻译 prompt(用户说"帮我推演分镜",就直接传"帮我推演分镜",不要自己先写个分镜表再逐条发)
|
|
@@ -355,4 +415,4 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
355
415
|
- 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
|
|
356
416
|
- 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下
|
|
357
417
|
- 生成过程中将过程中的创作信息展示给用户;任务完成后给出**产物结果(图片/视频)URL链接**和下载的**本地文件列表**。
|
|
358
|
-
-
|
|
418
|
+
- 图片/视频模型直出和视频处理任务必须保留 CLI 返回的 `thread_id` / `run_id`,并用 `query-result` 取回最终图片或视频。
|