Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ pippit-tool-cli generate-video \
--resolution "720p"
```

命令输出 `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` 和 `Seedance_2.5` 为 VIP 专属模型。CLI 会在提交前校验 prompt、素材数量和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
命令输出 `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`。图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频仅支持 `.mp3`、`.wav`。普通用户支持模型 `Seedance_2.0_mini_lite`;`seedance2.0_vision`、`seedance2.0_fast_vision`、`Seedance_2.0_mini` 和 `Seedance_2.5` 为 VIP 专属模型。CLI 会在提交前校验 prompt 和文件后缀;模型、比例、分辨率等语义校验由服务端处理。

首尾帧生视频时,按首帧、尾帧的顺序传入两次 `--image`,并设置 `--generate-type 1`:

Expand Down
6 changes: 3 additions & 3 deletions cmd/generate_video/generate_video.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
cmd.SetErr(stderr)
flags := cmd.Flags()
flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt")
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images, up to 9")
flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos, up to 3")
flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios, up to 3")
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos")
flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios")
flags.IntVar(&durationSec, "duration", 0, "video duration in seconds")
flags.StringVar(&opts.Ratio, "ratio", "", "video ratio, such as 9:16, 16:9, 3:4, 4:3")
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, Seedance_2.5")
Expand Down
112 changes: 57 additions & 55 deletions cmd/generate_video_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,72 +189,74 @@ func TestGenerateVideoRequiresPrompt(t *testing.T) {
}
}

func TestGenerateVideoRejectsTooManyImages(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("server should not receive request when image count is invalid")
}))
defer server.Close()

var stdout, stderr bytes.Buffer
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
args := []string{"generate-video", "--prompt", "x"}
for _, path := range mediaPaths("image", ".jpg", 10) {
args = append(args, "--image", path)
}
root.SetArgs(args)

err := root.Execute()
if err == nil {
t.Fatal("Execute() error = nil, want image count validation")
}
if !strings.Contains(err.Error(), "参考图片最多支持 9 个,当前传入 10 个") {
t.Fatalf("error = %q, want image count validation", err)
}
}

func TestGenerateVideoRejectsTooManyVideos(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("server should not receive request when video count is invalid")
}))
defer server.Close()

var stdout, stderr bytes.Buffer
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
func TestGenerateVideoAcceptsReferencesBeyondFormerLimits(t *testing.T) {
cwd := chdirTemp(t)
args := []string{"generate-video", "--prompt", "x"}
for _, path := range mediaPaths("video", ".mp4", 4) {
args = append(args, "--video", path)
}
root.SetArgs(args)

err := root.Execute()
if err == nil {
t.Fatal("Execute() error = nil, want video count validation")
}
if !strings.Contains(err.Error(), "参考视频最多支持 3 个,当前传入 4 个") {
t.Fatalf("error = %q, want video count validation", err)
var assetIDs []string
for _, media := range []struct {
flag string
ext string
count int
}{
{"image", ".jpg", 10},
{"video", ".mp4", 4},
{"audio", ".mp3", 4},
} {
for _, name := range mediaPaths(media.flag, media.ext, media.count) {
path := filepath.Join(cwd, name)
if err := os.WriteFile(path, []byte("media-data"), 0o644); err != nil {
t.Fatalf("WriteFile(%s): %v", path, err)
}
args = append(args, "--"+media.flag, path)
assetIDs = append(assetIDs, name)
}
}
}

func TestGenerateVideoRejectsTooManyAudios(t *testing.T) {
uploadIndex := 0
submitted := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("server should not receive request when audio count is invalid")
switch r.URL.Path {
case "/api/biz/v1/skill/upload_file":
if uploadIndex >= len(assetIDs) {
t.Fatalf("unexpected upload %d", uploadIndex)
}
_, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"` + assetIDs[uploadIndex] + `"}}`))
uploadIndex++
case "/api/biz/v1/skill/submit_run":
if uploadIndex != len(assetIDs) {
t.Fatalf("uploaded %d references, want %d", uploadIndex, len(assetIDs))
}
data, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
var body map[string]any
if err := sonic.Unmarshal(data, &body); err != nil {
t.Fatalf("decode body: %v", err)
}
param, ok := body["video_part_tool_param"].(map[string]any)
if !ok {
t.Fatalf("video_part_tool_param = %#v, want object", body["video_part_tool_param"])
}
assertAssetRefs(t, param["images"], assetIDs[:10])
assertAssetRefs(t, param["videos"], assetIDs[10:14])
assertAssetRefs(t, param["audios"], assetIDs[14:])
submitted = true
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread_123","run_id":"run_456"}}}`))
default:
t.Fatalf("unexpected path %s", r.URL.Path)
}
}))
defer server.Close()

var stdout, stderr bytes.Buffer
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
args := []string{"generate-video", "--prompt", "x"}
for _, path := range mediaPaths("audio", ".mp3", 4) {
args = append(args, "--audio", path)
}
root.SetArgs(args)

err := root.Execute()
if err == nil {
t.Fatal("Execute() error = nil, want audio count validation")
if err := root.Execute(); err != nil {
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
}
if !strings.Contains(err.Error(), "参考音频最多支持 3 个,当前传入 4 个") {
t.Fatalf("error = %q, want audio count validation", err)
if !submitted {
t.Fatal("generate-video did not submit references")
}
}

Expand Down
15 changes: 0 additions & 15 deletions internal/generate_video/generate_video.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import (
"github.com/Pippit-dev/pippit-cli/internal/common"
)

const (
maxReferenceImages = 9
maxReferenceVideos = 3
maxReferenceAudios = 3
)

var (
allowedImageExtensionList = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"}
allowedAudioExtensionList = []string{".mp3", ".wav"}
Expand Down Expand Up @@ -70,15 +64,6 @@ func ValidateOptions(opts *Options) error {
if strings.TrimSpace(opts.Prompt) == "" {
return fmt.Errorf("缺少必填参数 --prompt")
}
if len(opts.ImagePaths) > maxReferenceImages {
return fmt.Errorf("参考图片最多支持 %d 个,当前传入 %d 个", maxReferenceImages, len(opts.ImagePaths))
}
if len(opts.VideoPaths) > maxReferenceVideos {
return fmt.Errorf("参考视频最多支持 %d 个,当前传入 %d 个", maxReferenceVideos, len(opts.VideoPaths))
}
if len(opts.AudioPaths) > maxReferenceAudios {
return fmt.Errorf("参考音频最多支持 %d 个,当前传入 %d 个", maxReferenceAudios, len(opts.AudioPaths))
}
if err := validateMediaExtensions("图片", opts.ImagePaths, allowedImageExtensions, allowedImageExtensionList); err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pippit-dev/cli",
"version": "1.0.25",
"version": "1.0.26",
"description": "Pippit CLI",
"bin": {
"pippit-tool-cli": "scripts/run.js"
Expand Down
6 changes: 3 additions & 3 deletions skills/xyq-nest-skill/commands/generate-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
| --- | --- | --- |
| `--prompt` | 是 | 用户原始描述,不能全为空白 |
| `--model` | 否 | 用户指定的模型;未提供时省略,由服务端处理默认配置 |
| `--image` | 否 | 本地图片路径,重复参数,最多 9 张 |
| `--video` | 否 | 本地参考视频路径,重复参数,最多 3 个 |
| `--audio` | 否 | 本地 `.mp3/.wav` 音频路径,重复参数,最多 3 个 |
| `--image` | 否 | 本地图片路径,重复参数 |
| `--video` | 否 | 本地参考视频路径,重复参数 |
| `--audio` | 否 | 本地 `.mp3/.wav` 音频路径,重复参数 |
| `--duration` | 否 | 整数秒;用户只给时长范围时先确认具体秒数 |
| `--ratio` | 否 | 比例字符串,如 `9:16`、`16:9`、`3:4`、`4:3`;不转换为生图枚举 |
| `--resolution` | 否 | 用户指定值,如 `720p`、`1080p` |
Expand Down
Loading