@pippit-dev/cli 1.0.6 → 1.0.8
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 +11 -7
- package/checksums.txt +6 -6
- package/cmd/generate_video/generate_video.go +3 -1
- package/cmd/generate_video_test.go +56 -4
- package/internal/common/upload_file.go +8 -0
- package/internal/generate_video/generate_video.go +18 -2
- package/package.json +1 -1
- package/skills/xyq-nest-skill/SKILL.md +18 -13
- package/skills/xyq-nest-skill/scripts/upload_file.py +18 -5
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
| 技能 | 说明 | 路径 |
|
|
10
10
|
|-------|-------------|------|
|
|
11
11
|
| `xyq-short-drama-skill` | 短剧工作流技能,支持提交创作任务、上传参考文件、查询进度、列出会话文件和下载产物。 | `skills/short-drama/` |
|
|
12
|
-
| `xyq-skill` | 通用 NestAgent
|
|
12
|
+
| `xyq-skill` | 通用 NestAgent 技能,支持图片/视频生成、编辑、图片/视频/mp3或wav音频文件上传、进度查询和结果下载。 | `skills/xyq-nest-skill/` |
|
|
13
13
|
|
|
14
14
|
## 通用 NestAgent 技能
|
|
15
15
|
|
|
16
|
-
`xyq-skill` 通过接入小云雀 NestAgent 的综合创作能力,实现 AI
|
|
16
|
+
`xyq-skill` 通过接入小云雀 NestAgent 的综合创作能力,实现 AI 图片/视频生成、编辑、风格转换、图片/视频/mp3或wav音频文件上传、进度查询和结果下载。
|
|
17
17
|
|
|
18
18
|
### 功能特性
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|------|------|
|
|
22
22
|
| 创建会话 / 发送消息 | 向小云雀发送自然语言指令,生成图片或视频。 |
|
|
23
23
|
| 查询会话进展 | 增量拉取会话消息,轮询创作进度和产物结果。 |
|
|
24
|
-
| 上传文件 |
|
|
24
|
+
| 上传文件 | 上传图片/视频/mp3或wav音频到小云雀资产库,获取 `asset_id` 用于编辑和参考。 |
|
|
25
25
|
| 下载结果 | 批量下载生成的图片/视频到本地,支持并行下载。 |
|
|
26
26
|
|
|
27
27
|
小云雀平台能力覆盖:
|
|
@@ -103,9 +103,12 @@ python3 skills/xyq-nest-skill/scripts/upload_file.py /path/to/image.png
|
|
|
103
103
|
|
|
104
104
|
# 上传视频
|
|
105
105
|
python3 skills/xyq-nest-skill/scripts/upload_file.py /path/to/video.mp4
|
|
106
|
+
|
|
107
|
+
# 上传音频
|
|
108
|
+
python3 skills/xyq-nest-skill/scripts/upload_file.py /path/to/audio.mp3
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
仅支持 `image/*` 和 `
|
|
111
|
+
仅支持 `image/*`、`video/*` 和 `.mp3/.wav` 音频文件,单文件大小限制 200 MB。
|
|
109
112
|
|
|
110
113
|
返回示例:
|
|
111
114
|
|
|
@@ -212,7 +215,7 @@ pippit-tool-cli download-result --output-path ./thread_123/results/result.mp4 --
|
|
|
212
215
|
|
|
213
216
|
## 生视频 CLI
|
|
214
217
|
|
|
215
|
-
`generate-video`
|
|
218
|
+
`generate-video` 会上传本地参考图片、视频和音频,然后向视频片段 Agent 提交生视频请求:
|
|
216
219
|
|
|
217
220
|
```bash
|
|
218
221
|
pippit-tool-cli generate-video \
|
|
@@ -221,13 +224,14 @@ pippit-tool-cli generate-video \
|
|
|
221
224
|
--image "~/images/cat2.jpg" \
|
|
222
225
|
--video "~/images/video1.mp4" \
|
|
223
226
|
--video "~/images/video2.mp4" \
|
|
227
|
+
--audio "~/audio/bgm.mp3" \
|
|
224
228
|
--duration 5 \
|
|
225
229
|
--ratio "9:16" \
|
|
226
|
-
--model "
|
|
230
|
+
--model "seedance2.0_direct" \
|
|
227
231
|
--resolution "720p"
|
|
228
232
|
```
|
|
229
233
|
|
|
230
|
-
命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP
|
|
234
|
+
命令输出 `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`。普通用户支持模型 `seedance2.0_direct` 和 `seedance2.0_fast_direct`;VIP 用户额外支持 `seedance2.0_vision` 和 `seedance2.0_fast_vision`。模型、比例、分辨率、素材数量等参数的语义校验当前预留在后续实现。
|
|
231
235
|
|
|
232
236
|
查询并下载生视频结果:
|
|
233
237
|
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
0868a4798bb8ced3bcf2d1c53938457ef311920f3f4c6fff97c7d78cedcd68d2 pippit-tool-cli-1.0.8-darwin-amd64.tar.gz
|
|
2
|
+
1e4d0bace4be4e47fd11e05056cb0953fb63236ceb240370302d6d680f504d85 pippit-tool-cli-1.0.8-darwin-arm64.tar.gz
|
|
3
|
+
c23a7f3a4c3d8e2840a02e40eb6df6a7cfa8d13384315953afe39f216ee12a0d pippit-tool-cli-1.0.8-linux-amd64.tar.gz
|
|
4
|
+
d079ff9915ee2c0676d15d619390e9d5e13290381f2cbbd450d9c34996dd5915 pippit-tool-cli-1.0.8-linux-arm64.tar.gz
|
|
5
|
+
34ed2d0fa62061d39f3787a56f95bca4f55d363e47d94ada9b0090d3845737a2 pippit-tool-cli-1.0.8-windows-amd64.zip
|
|
6
|
+
6133c858619c795b4e22e5e701313a5665ea70c885cd40c21aef514c3b452621 pippit-tool-cli-1.0.8-windows-arm64.zip
|
|
@@ -28,6 +28,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
28
28
|
_ = common.AppendDailyErrorLog("generate-video", err, map[string]string{
|
|
29
29
|
"image_count": strconv.Itoa(len(opts.ImagePaths)),
|
|
30
30
|
"video_count": strconv.Itoa(len(opts.VideoPaths)),
|
|
31
|
+
"audio_count": strconv.Itoa(len(opts.AudioPaths)),
|
|
31
32
|
})
|
|
32
33
|
return err
|
|
33
34
|
}
|
|
@@ -40,9 +41,10 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
40
41
|
flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt")
|
|
41
42
|
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images, up to 9")
|
|
42
43
|
flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos, up to 3")
|
|
44
|
+
flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios, up to 3")
|
|
43
45
|
flags.IntVar(&durationSec, "duration", 0, "video duration in seconds")
|
|
44
46
|
flags.StringVar(&opts.Ratio, "ratio", "", "video ratio, such as 9:16, 16:9, 3:4, 4:3")
|
|
45
|
-
flags.StringVar(&opts.Model, "model", "", "video model; normal users: seedance2.0_direct, seedance2.0_fast_direct; VIP users: seedance2.0_direct, seedance2.0_fast_direct, seedance2.0_vision, seedance2.0_fast_vision
|
|
47
|
+
flags.StringVar(&opts.Model, "model", "", "video model; normal users: seedance2.0_direct, seedance2.0_fast_direct; VIP users: seedance2.0_direct, seedance2.0_fast_direct, seedance2.0_vision, seedance2.0_fast_vision")
|
|
46
48
|
flags.StringVar(&opts.Resolution, "resolution", "", "video resolution, such as 720p, 1080p")
|
|
47
49
|
return cmd
|
|
48
50
|
}
|
|
@@ -14,7 +14,7 @@ import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
func TestGenerateVideo(t *testing.T) {
|
|
17
|
-
assetIDs := []string{"image_asset_1", "image_asset_2", "video_asset_1", "video_asset_2"}
|
|
17
|
+
assetIDs := []string{"image_asset_1", "image_asset_2", "video_asset_1", "video_asset_2", "audio_asset_1"}
|
|
18
18
|
uploadIndex := 0
|
|
19
19
|
|
|
20
20
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
@@ -36,6 +36,9 @@ func TestGenerateVideo(t *testing.T) {
|
|
|
36
36
|
if len(files) != 1 {
|
|
37
37
|
t.Fatalf("file parts = %d, want 1", len(files))
|
|
38
38
|
}
|
|
39
|
+
if files[0].Filename == "bgm.wav" && !strings.HasPrefix(files[0].Header.Get("Content-Type"), "audio/") {
|
|
40
|
+
t.Fatalf("audio content type = %q, want audio content type", files[0].Header.Get("Content-Type"))
|
|
41
|
+
}
|
|
39
42
|
uploadIndex++
|
|
40
43
|
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"` + assetIDs[uploadIndex-1] + `"}}`))
|
|
41
44
|
case "/api/biz/v1/skill/submit_run":
|
|
@@ -66,11 +69,12 @@ func TestGenerateVideo(t *testing.T) {
|
|
|
66
69
|
if param["duration_sec"] != float64(5) {
|
|
67
70
|
t.Fatalf("duration_sec = %v, want 5", param["duration_sec"])
|
|
68
71
|
}
|
|
69
|
-
if param["ratio"] != "9:16" || param["model"] != "
|
|
72
|
+
if param["ratio"] != "9:16" || param["model"] != "seedance2.0_vision" || param["resolution"] != "720p" {
|
|
70
73
|
t.Fatalf("param = %#v, want ratio/model/resolution", param)
|
|
71
74
|
}
|
|
72
75
|
assertAssetRefs(t, param["images"], []string{"image_asset_1", "image_asset_2"})
|
|
73
76
|
assertAssetRefs(t, param["videos"], []string{"video_asset_1", "video_asset_2"})
|
|
77
|
+
assertAssetRefs(t, param["audios"], []string{"audio_asset_1"})
|
|
74
78
|
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_123","run_id":"run_456"},"web_thread_link":"https://xyq.example/thread_123"}}`))
|
|
75
79
|
default:
|
|
76
80
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
@@ -83,7 +87,8 @@ func TestGenerateVideo(t *testing.T) {
|
|
|
83
87
|
image2 := filepath.Join(cwd, "cat2.jpg")
|
|
84
88
|
video1 := filepath.Join(cwd, "video1.mp4")
|
|
85
89
|
video2 := filepath.Join(cwd, "video2.mp4")
|
|
86
|
-
|
|
90
|
+
audio1 := filepath.Join(cwd, "bgm.wav")
|
|
91
|
+
for _, path := range []string{image1, image2, video1, video2, audio1} {
|
|
87
92
|
if err := os.WriteFile(path, []byte("media-data"), 0o644); err != nil {
|
|
88
93
|
t.Fatalf("WriteFile(%s): %v", path, err)
|
|
89
94
|
}
|
|
@@ -98,9 +103,10 @@ func TestGenerateVideo(t *testing.T) {
|
|
|
98
103
|
"--image", image2,
|
|
99
104
|
"--video", video1,
|
|
100
105
|
"--video", video2,
|
|
106
|
+
"--audio", audio1,
|
|
101
107
|
"--duration", "5",
|
|
102
108
|
"--ratio", "9:16",
|
|
103
|
-
"--model", "
|
|
109
|
+
"--model", "seedance2.0_vision",
|
|
104
110
|
"--resolution", "720p",
|
|
105
111
|
})
|
|
106
112
|
|
|
@@ -209,6 +215,52 @@ func TestGenerateVideoRejectsTooManyVideos(t *testing.T) {
|
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
func TestGenerateVideoRejectsTooManyAudios(t *testing.T) {
|
|
219
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
220
|
+
t.Fatal("server should not receive request when audio count is invalid")
|
|
221
|
+
}))
|
|
222
|
+
defer server.Close()
|
|
223
|
+
|
|
224
|
+
var stdout, stderr bytes.Buffer
|
|
225
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
226
|
+
args := []string{"generate-video", "--prompt", "x"}
|
|
227
|
+
for _, path := range mediaPaths("audio", ".mp3", 4) {
|
|
228
|
+
args = append(args, "--audio", path)
|
|
229
|
+
}
|
|
230
|
+
root.SetArgs(args)
|
|
231
|
+
|
|
232
|
+
err := root.Execute()
|
|
233
|
+
if err == nil {
|
|
234
|
+
t.Fatal("Execute() error = nil, want audio count validation")
|
|
235
|
+
}
|
|
236
|
+
if !strings.Contains(err.Error(), "参考音频最多支持 3 个,当前传入 4 个") {
|
|
237
|
+
t.Fatalf("error = %q, want audio count validation", err)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func TestGenerateVideoRejectsUnsupportedAudioExtension(t *testing.T) {
|
|
242
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
243
|
+
t.Fatal("server should not receive request when audio extension is invalid")
|
|
244
|
+
}))
|
|
245
|
+
defer server.Close()
|
|
246
|
+
|
|
247
|
+
var stdout, stderr bytes.Buffer
|
|
248
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
249
|
+
root.SetArgs([]string{
|
|
250
|
+
"generate-video",
|
|
251
|
+
"--prompt", "x",
|
|
252
|
+
"--audio", "bgm.wma",
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
err := root.Execute()
|
|
256
|
+
if err == nil {
|
|
257
|
+
t.Fatal("Execute() error = nil, want audio extension validation")
|
|
258
|
+
}
|
|
259
|
+
if !strings.Contains(err.Error(), `不支持的音频文件后缀 ".wma"`) {
|
|
260
|
+
t.Fatalf("error = %q, want audio extension validation", err)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
212
264
|
func TestGenerateVideoSubmitRunErrorIncludesLogID(t *testing.T) {
|
|
213
265
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
214
266
|
switch r.URL.Path {
|
|
@@ -36,6 +36,11 @@ type uploadFileResponse struct {
|
|
|
36
36
|
|
|
37
37
|
const uploadFileFieldName = "file"
|
|
38
38
|
|
|
39
|
+
var uploadFileContentTypeFallbacks = map[string]string{
|
|
40
|
+
".mp3": "audio/mpeg",
|
|
41
|
+
".wav": "audio/wav",
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
func UploadFile(ctx context.Context, opts UploadFileOptions, runner *Runner) (*UploadFileResult, error) {
|
|
40
45
|
if err := ctx.Err(); err != nil {
|
|
41
46
|
if errors.Is(err, context.Canceled) {
|
|
@@ -71,6 +76,9 @@ func UploadFile(ctx context.Context, opts UploadFileOptions, runner *Runner) (*U
|
|
|
71
76
|
ext := strings.ToLower(filepath.Ext(path))
|
|
72
77
|
fileName := filepath.Base(path)
|
|
73
78
|
contentType := mime.TypeByExtension(ext)
|
|
79
|
+
if contentType == "" {
|
|
80
|
+
contentType = uploadFileContentTypeFallbacks[ext]
|
|
81
|
+
}
|
|
74
82
|
|
|
75
83
|
var resp uploadFileResponse
|
|
76
84
|
if err := runner.Client.SendMultipartRequest(ctx, uploadFilePath(runner), nil, MultipartFile{
|
|
@@ -14,13 +14,16 @@ const (
|
|
|
14
14
|
agentNameVideoPart = "pippit_video_part_agent"
|
|
15
15
|
maxReferenceImages = 9
|
|
16
16
|
maxReferenceVideos = 3
|
|
17
|
+
maxReferenceAudios = 3
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
var (
|
|
20
21
|
allowedImageExtensionList = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"}
|
|
21
22
|
allowedVideoExtensionList = []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v"}
|
|
23
|
+
allowedAudioExtensionList = []string{".mp3", ".wav"}
|
|
22
24
|
allowedImageExtensions = makeExtensionSet(allowedImageExtensionList)
|
|
23
25
|
allowedVideoExtensions = makeExtensionSet(allowedVideoExtensionList)
|
|
26
|
+
allowedAudioExtensions = makeExtensionSet(allowedAudioExtensionList)
|
|
24
27
|
)
|
|
25
28
|
|
|
26
29
|
// Options is the stable command-facing request shape for generate-video.
|
|
@@ -28,6 +31,7 @@ type Options struct {
|
|
|
28
31
|
Prompt string
|
|
29
32
|
ImagePaths []string
|
|
30
33
|
VideoPaths []string
|
|
34
|
+
AudioPaths []string
|
|
31
35
|
DurationSec *int
|
|
32
36
|
Ratio string
|
|
33
37
|
Model string
|
|
@@ -44,6 +48,7 @@ type videoPartToolParam struct {
|
|
|
44
48
|
DurationSec *int `json:"duration_sec,omitempty"`
|
|
45
49
|
Ratio string `json:"ratio,omitempty"`
|
|
46
50
|
Videos []mediaAsset `json:"videos,omitempty"`
|
|
51
|
+
Audios []mediaAsset `json:"audios,omitempty"`
|
|
47
52
|
Model string `json:"model,omitempty"`
|
|
48
53
|
Resolution string `json:"resolution,omitempty"`
|
|
49
54
|
}
|
|
@@ -71,8 +76,12 @@ func Run(ctx context.Context, opts *Options, runner *common.Runner) (*Result, er
|
|
|
71
76
|
if err != nil {
|
|
72
77
|
return nil, fmt.Errorf("上传视频失败: %w", err)
|
|
73
78
|
}
|
|
79
|
+
audioAssetIDs, err := uploadMediaList(ctx, opts.AudioPaths, runner)
|
|
80
|
+
if err != nil {
|
|
81
|
+
return nil, fmt.Errorf("上传音频失败: %w", err)
|
|
82
|
+
}
|
|
74
83
|
|
|
75
|
-
body := buildSubmitRunBody(opts, imageAssetIDs, videoAssetIDs)
|
|
84
|
+
body := buildSubmitRunBody(opts, imageAssetIDs, videoAssetIDs, audioAssetIDs)
|
|
76
85
|
|
|
77
86
|
var resp common.SubmitRunResponse
|
|
78
87
|
if err := runner.Client.SendRequest(ctx, common.SubmitRunPath(runner), body, &resp); err != nil {
|
|
@@ -111,12 +120,18 @@ func ValidateOptions(opts *Options) error {
|
|
|
111
120
|
if len(opts.VideoPaths) > maxReferenceVideos {
|
|
112
121
|
return fmt.Errorf("参考视频最多支持 %d 个,当前传入 %d 个", maxReferenceVideos, len(opts.VideoPaths))
|
|
113
122
|
}
|
|
123
|
+
if len(opts.AudioPaths) > maxReferenceAudios {
|
|
124
|
+
return fmt.Errorf("参考音频最多支持 %d 个,当前传入 %d 个", maxReferenceAudios, len(opts.AudioPaths))
|
|
125
|
+
}
|
|
114
126
|
if err := validateMediaExtensions("图片", opts.ImagePaths, allowedImageExtensions, allowedImageExtensionList); err != nil {
|
|
115
127
|
return err
|
|
116
128
|
}
|
|
117
129
|
if err := validateMediaExtensions("视频", opts.VideoPaths, allowedVideoExtensions, allowedVideoExtensionList); err != nil {
|
|
118
130
|
return err
|
|
119
131
|
}
|
|
132
|
+
if err := validateMediaExtensions("音频", opts.AudioPaths, allowedAudioExtensions, allowedAudioExtensionList); err != nil {
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
120
135
|
return nil
|
|
121
136
|
}
|
|
122
137
|
|
|
@@ -154,13 +169,14 @@ func uploadMediaList(ctx context.Context, paths []string, runner *common.Runner)
|
|
|
154
169
|
return assetIDs, nil
|
|
155
170
|
}
|
|
156
171
|
|
|
157
|
-
func buildSubmitRunBody(opts *Options, imageAssetIDs []string, videoAssetIDs []string) map[string]any {
|
|
172
|
+
func buildSubmitRunBody(opts *Options, imageAssetIDs []string, videoAssetIDs []string, audioAssetIDs []string) map[string]any {
|
|
158
173
|
param := videoPartToolParam{
|
|
159
174
|
Images: assetRefs(imageAssetIDs),
|
|
160
175
|
Prompt: strings.TrimSpace(opts.Prompt),
|
|
161
176
|
DurationSec: opts.DurationSec,
|
|
162
177
|
Ratio: strings.TrimSpace(opts.Ratio),
|
|
163
178
|
Videos: assetRefs(videoAssetIDs),
|
|
179
|
+
Audios: assetRefs(audioAssetIDs),
|
|
164
180
|
Model: strings.TrimSpace(opts.Model),
|
|
165
181
|
Resolution: strings.TrimSpace(opts.Resolution),
|
|
166
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xyq-skill
|
|
3
|
-
description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频。覆盖场景包括:生成(文生图、文生视频、图生视频、做动画、画一个xxx、来段xxx)、编辑修改(把xxx换成yyy、去掉xxx、加上xxx、改成xxx、调整xxx、局部修改、改镜头)、风格转换(风格迁移、转绘、换风格)、视频续写延长、复刻视频/TVC/宣传片、短剧/短漫剧生成、音乐MV生成、产品广告/展示片制作、分镜/故事板设计、教育视频/短视频制作。当用户提到小云雀、xyq
|
|
3
|
+
description: 通过小云雀的 AI 能力进行综合创作,支持生成和编辑图片/视频。覆盖场景包括:生成(文生图、文生视频、图生视频、做动画、画一个xxx、来段xxx)、编辑修改(把xxx换成yyy、去掉xxx、加上xxx、改成xxx、调整xxx、局部修改、改镜头)、风格转换(风格迁移、转绘、换风格)、视频续写延长、复刻视频/TVC/宣传片、短剧/短漫剧生成、音乐MV生成、产品广告/展示片制作、分镜/故事板设计、教育视频/短视频制作。当用户提到小云雀、xyq、上传参考图/视频/mp3或wav音频、查看生成进度时也应触发。关键判断:只要用户的请求涉及 AI 视频的创作、生成、编辑、修改,无论措辞如何(如"画只猫"、"做个海报"、"做个视频"、"这个视频帮我改一下"、"帮我复刻这段视频"、"用这首歌做个MV"、"一句话生成短剧"),都必须触发此技能。
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata:
|
|
6
6
|
{
|
|
@@ -19,7 +19,7 @@ metadata:
|
|
|
19
19
|
|
|
20
20
|
# 小云雀会话(生视频)
|
|
21
21
|
|
|
22
|
-
通过 小云雀的API
|
|
22
|
+
通过 小云雀的API 创建会话、发送消息(生图、生视频、编辑视频等)、上传图片/视频/mp3或wav音频文件,并查询会话消息进展。
|
|
23
23
|
|
|
24
24
|
小云雀是一个 AI 综合创作平台,同时为人类创作者和 Agent 设计。Agent 通过 Skill 入口理解任务、调用模型并自动编排工作流。
|
|
25
25
|
|
|
@@ -34,7 +34,7 @@ metadata:
|
|
|
34
34
|
|
|
35
35
|
1. **创建会话 / 发消息** - 创建新会话或向已有会话发送一条消息(如「创作一个视频」)
|
|
36
36
|
2. **查询会话进展** - 根据 `thread_id` 、 `run_id`、`after_seq` 增量拉取该会话的消息列表,用于轮询创作过程的消息和最终产物结果
|
|
37
|
-
3. **上传文件** -
|
|
37
|
+
3. **上传文件** - 支持上传`单张图片`、`单个视频文件`或`单个mp3/wav音频文件`到小云雀资产库,得到文件对应的 `asset_id`(编辑或参考已有图片/视频/音频时需要先上传)
|
|
38
38
|
4. **下载结果** - 将会话中生成的图片/视频批量下载到本地,支持指定输出目录和文件名前缀。
|
|
39
39
|
|
|
40
40
|
|
|
@@ -71,7 +71,7 @@ python3 {baseDir}/scripts/get_thread.py --thread-id THREAD_ID --run-id RUN_ID --
|
|
|
71
71
|
|
|
72
72
|
### 3. 上传文件
|
|
73
73
|
|
|
74
|
-
-
|
|
74
|
+
- 当用户提供了参考的文件地址时,先进行文件上传,仅支持图片、视频、`.mp3/.wav` 音频。
|
|
75
75
|
- 单次指令执行仅支持单个文件,多个文件可并行调用,单个文件大小必须在200MB以下。
|
|
76
76
|
|
|
77
77
|
```bash
|
|
@@ -80,6 +80,9 @@ python3 {baseDir}/scripts/upload_file.py /path/to/image.png
|
|
|
80
80
|
|
|
81
81
|
# 上传视频
|
|
82
82
|
python3 {baseDir}/scripts/upload_file.py /path/to/video.mp4
|
|
83
|
+
|
|
84
|
+
# 上传音频
|
|
85
|
+
python3 {baseDir}/scripts/upload_file.py /path/to/audio.mp3
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
### 4. 下载结果
|
|
@@ -115,24 +118,26 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
115
118
|
6. 向用户展示:过程中的创作信息,以及下载后的本地文件列表
|
|
116
119
|
```
|
|
117
120
|
|
|
118
|
-
### 场景 2
|
|
121
|
+
### 场景 2:用户提供图片/视频/音频要求编辑修改或作为参考(如"参考这个视频做一个新的"、"用这首歌做MV")
|
|
119
122
|
|
|
120
123
|
```
|
|
121
|
-
1. upload_file.py /path/to/video.mp4 → 拿到
|
|
122
|
-
2.
|
|
123
|
-
3.
|
|
124
|
+
1. upload_file.py /path/to/video.mp4 → 拿到 asset_id1
|
|
125
|
+
2. upload_file.py /path/to/audio.mp3 → 拿到 asset_id2
|
|
126
|
+
3. submit_run.py --message "参考这个视频并用这首歌做一个新的" --asset-ids asset_id1 asset_id2 → 拿到 thread_id、run_id、web_thread_link
|
|
127
|
+
4. 后续同场景 1 的步骤 2-6
|
|
124
128
|
```
|
|
125
129
|
|
|
126
130
|
用户给了文件路径 + 编辑指令 = 先上传文件,再把编辑指令和 所有asset_id 一起发送。
|
|
127
131
|
|
|
128
|
-
### 场景 3
|
|
132
|
+
### 场景 3:用户提供参考图/视频/音频要求生成新内容
|
|
129
133
|
|
|
130
134
|
```
|
|
131
135
|
1. upload_file.py /path/to/ref1.png → 拿到 asset_id1
|
|
132
136
|
2. upload_file.py /path/to/ref2.mp4 → 拿到 asset_id2
|
|
133
|
-
3.
|
|
134
|
-
4.
|
|
135
|
-
5.
|
|
137
|
+
3. upload_file.py /path/to/ref3.mp3 → 拿到 asset_id3
|
|
138
|
+
4. 直到所有文件上传完成,拿到所有 asset_id
|
|
139
|
+
5. submit_run.py --message "根据参考图、视频、音频生成xxx" --asset-ids asset_id1 asset_id2 asset_id3, ... → 拿到 thread_id、run_id、web_thread_link
|
|
140
|
+
6. 后续同场景 1 的步骤 2-6
|
|
136
141
|
```
|
|
137
142
|
|
|
138
143
|
### 场景 4:在已有会话中追加新需求
|
|
@@ -251,5 +256,5 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
|
|
|
251
256
|
- 鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
|
|
252
257
|
- 创建会话时 `message` 是用户的指令要求,不能为空
|
|
253
258
|
- 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
|
|
254
|
-
- 上传文件仅支持图片(image
|
|
259
|
+
- 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下
|
|
255
260
|
- 生成过程中将过程中的创作信息展示给用户;任务完成后给出**产物结果(图片/视频)URL链接**和下载的**本地文件列表**。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""上传图片/视频/mp3或wav音频到小云雀资产库:POST /api/biz/v1/skill/upload_file(multipart/form-data)"""
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
import json
|
|
@@ -15,6 +15,11 @@ from xyq_common import XYQ_BASE, ACCESS_KEY, UPLOAD_FILE_PATH, HTTP_TIMEOUT_SECO
|
|
|
15
15
|
|
|
16
16
|
# 允许的 MIME 类型前缀
|
|
17
17
|
ALLOWED_PREFIXES = ("image/", "video/")
|
|
18
|
+
ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav"}
|
|
19
|
+
EXTRA_MIME_MAP = {
|
|
20
|
+
".mp3": "audio/mpeg",
|
|
21
|
+
".wav": "audio/wav",
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
def upload_file(file_path: str) -> dict:
|
|
@@ -27,9 +32,14 @@ def upload_file(file_path: str) -> dict:
|
|
|
27
32
|
sys.exit(1)
|
|
28
33
|
|
|
29
34
|
# 检查 MIME 类型
|
|
35
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
30
36
|
mime_type, _ = mimetypes.guess_type(file_path)
|
|
31
|
-
if
|
|
32
|
-
|
|
37
|
+
if not mime_type:
|
|
38
|
+
mime_type = EXTRA_MIME_MAP.get(ext)
|
|
39
|
+
is_supported_media = bool(mime_type) and any(mime_type.startswith(p) for p in ALLOWED_PREFIXES)
|
|
40
|
+
is_supported_audio = bool(mime_type) and mime_type.startswith("audio/") and ext in ALLOWED_AUDIO_EXTENSIONS
|
|
41
|
+
if not is_supported_media and not is_supported_audio:
|
|
42
|
+
print(f"错误:不支持的文件类型: {mime_type or '未知'},仅支持图片、视频和 .mp3/.wav 音频", file=sys.stderr)
|
|
33
43
|
sys.exit(1)
|
|
34
44
|
|
|
35
45
|
# 构建 multipart/form-data 请求体
|
|
@@ -84,7 +94,7 @@ def upload_file(file_path: str) -> dict:
|
|
|
84
94
|
|
|
85
95
|
def main():
|
|
86
96
|
parser = argparse.ArgumentParser(
|
|
87
|
-
description="
|
|
97
|
+
description="上传图片、视频或 mp3/wav 音频文件到小云雀资产库",
|
|
88
98
|
epilog="""
|
|
89
99
|
环境变量:
|
|
90
100
|
XYQ_ACCESS_KEY 必填,Bearer 鉴权
|
|
@@ -96,12 +106,15 @@ def main():
|
|
|
96
106
|
|
|
97
107
|
# 上传视频
|
|
98
108
|
python3 upload_file.py /path/to/video.mp4
|
|
109
|
+
|
|
110
|
+
# 上传音频
|
|
111
|
+
python3 upload_file.py /path/to/audio.mp3
|
|
99
112
|
""",
|
|
100
113
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
101
114
|
)
|
|
102
115
|
parser.add_argument(
|
|
103
116
|
"file",
|
|
104
|
-
help="
|
|
117
|
+
help="要上传的图片、视频或 mp3/wav 音频文件路径",
|
|
105
118
|
)
|
|
106
119
|
args = parser.parse_args()
|
|
107
120
|
|