@pippit-dev/cli 1.0.7 → 1.0.9

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 CHANGED
@@ -9,11 +9,11 @@
9
9
  | 技能 | 说明 | 路径 |
10
10
  |-------|-------------|------|
11
11
  | `xyq-short-drama-skill` | 短剧工作流技能,支持提交创作任务、上传参考文件、查询进度、列出会话文件和下载产物。 | `skills/short-drama/` |
12
- | `xyq-skill` | 通用 NestAgent 技能,支持图片/视频生成、编辑、文件上传、进度查询和结果下载。 | `skills/xyq-nest-skill/` |
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
- | 上传文件 | 上传图片/视频到小云雀资产库,获取 `asset_id` 用于编辑和参考。 |
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/*` 和 `video/*` 类型,单文件大小限制 200 MB。
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` 会上传本地参考图片和视频,然后向视频片段 Agent 提交生视频请求:
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 "Seedance_2.0_mini" \
230
+ --model "Seedance_2.0_mini_lite" \
227
231
  --resolution "720p"
228
232
  ```
229
233
 
230
- 命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP 请求时,参考图和参考视频会使用上传接口返回的 `pippit_asset_id`。图片最多 9 张,支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频最多 3 个,支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`。普通用户支持模型 `seedance2.0_direct` 和 `seedance2.0_fast_direct`;VIP 用户额外支持 `seedance2.0_vision`、`seedance2.0_fast_vision` 和 `Seedance_2.0_mini`。模型、比例、分辨率、素材数量等参数的语义校验当前预留在后续实现。
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`。普通用户支持模型 `Seedance_2.0_mini_lite`;`seedance2.0_direct`、`seedance2.0_fast_direct`、`seedance2.0_vision`、`seedance2.0_fast_vision` 和 `Seedance_2.0_mini` 为 VIP 专属模型。CLI 会在提交前校验 prompt、素材数量和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
231
235
 
232
236
  查询并下载生视频结果:
233
237
 
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- a0c76ece08d3a2d862c42c5b7b7c18c26bddb70515d377f5f348b790b063c408 pippit-tool-cli-1.0.7-darwin-amd64.tar.gz
2
- 3ac483fd72d418dc122b1715508799d0145d5b0a4d34a67e14737b991205c5b6 pippit-tool-cli-1.0.7-darwin-arm64.tar.gz
3
- b60665ff4943b78d3f471117b8102d25ccf1702f7f2be77b9a72e7165eeafd37 pippit-tool-cli-1.0.7-linux-amd64.tar.gz
4
- 7e8eefeb90c48add42aa4ba7e928dc45ae83d163fde263d390857dd20cf4c239 pippit-tool-cli-1.0.7-linux-arm64.tar.gz
5
- 9a16066735c5624720e796771fd71d76d268709f18939904619d9a51d9985654 pippit-tool-cli-1.0.7-windows-amd64.zip
6
- c44215200e681f0980979178ef8a9435c22cdb64c01ce2f48168d07b9c9fe565 pippit-tool-cli-1.0.7-windows-arm64.zip
1
+ f74cc6e3fc592b21f827006f325558eb95366311e67cc9d9e426a459476a3996 pippit-tool-cli-1.0.9-darwin-amd64.tar.gz
2
+ dad92b312b3aee1d28e27fb781ab3c151f690f8fe1c97e19e776ac621cd153f6 pippit-tool-cli-1.0.9-darwin-arm64.tar.gz
3
+ 58db5bf7b5bdee7453983bf361493fa3da733d7409e854e960a262ad7373e292 pippit-tool-cli-1.0.9-linux-amd64.tar.gz
4
+ 5a414fab07508f04241c30b15847542730a913d7235441ce2ff74775506943ac pippit-tool-cli-1.0.9-linux-arm64.tar.gz
5
+ fd22dff28d01c1b304ecbc85b192ffbcbd0ef4e098e5d7347f9d37677b2bb9c9 pippit-tool-cli-1.0.9-windows-amd64.zip
6
+ 7e26c5053609f5cefa813654b7e752f3a77c90103c3509c5d71028a432b67386 pippit-tool-cli-1.0.9-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, Seedance_2.0_mini")
47
+ flags.StringVar(&opts.Model, "model", "", "video model; normal users: Seedance_2.0_mini_lite; VIP-only: seedance2.0_direct, seedance2.0_fast_direct, seedance2.0_vision, seedance2.0_fast_vision, Seedance_2.0_mini")
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"] != "Seedance_2.0_mini" || param["resolution"] != "720p" {
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
- for _, path := range []string{image1, image2, video1, video2} {
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", "Seedance_2.0_mini",
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": "@pippit-dev/cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -4,7 +4,7 @@ const path = require("path");
4
4
  const { runSilent } = require("./platform");
5
5
  const { DEFAULT_PKG } = require("./skills");
6
6
 
7
- const CHECK_INTERVAL_MS = 60 * 60 * 1000;
7
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
8
8
 
9
9
  function defaultCacheFile() {
10
10
  return path.join(os.homedir(), ".pippit_tool_cli", "version-check.json");
@@ -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、上传参考图/视频、查看生成进度时也应触发。关键判断:只要用户的请求涉及 AI 视频的创作、生成、编辑、修改,无论措辞如何(如"画只猫"、"做个海报"、"做个视频"、"这个视频帮我改一下"、"帮我复刻这段视频"、"用这首歌做个MV"、"一句话生成短剧"),都必须触发此技能。
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. **上传文件** - 支持上传`单张图片`或`单个视频文件`到小云雀资产库,得到文件对应的 `asset_id`(编辑已有视频/图片时需要先上传)
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 → 拿到 asset_id
122
- 2. submit_run.py --message "参考这个视频做一个新的" --asset-ids asset_id → 拿到 thread_id、run_id、web_thread_link
123
- 3. 后续同场景 1 的步骤 2-6
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. 直到所有文件上传完成,拿到所有 asset_id
134
- 4. submit_run.py --message "根据参考图、视频生成xxx" --asset-ids asset_id1 asset_id2, ... → 拿到 thread_id、run_id、web_thread_link
135
- 5. 后续同场景 1 的步骤 2-6
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/*)和视频(video/*)类型,其他类型会被拒绝,文件大小须在 200MB 以下
259
+ - 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下
255
260
  - 生成过程中将过程中的创作信息展示给用户;任务完成后给出**产物结果(图片/视频)URL链接**和下载的**本地文件列表**。
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """上传图片/视频到小云雀资产库:POST /api/biz/v1/skill/upload_file(multipart/form-data)"""
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 mime_type and not any(mime_type.startswith(p) for p in ALLOWED_PREFIXES):
32
- print(f"错误:不支持的文件类型: {mime_type},仅支持图片和视频", file=sys.stderr)
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