feat(ui): Video playground tab + video capability plumbing
New Video tab in the Playground: model selector filtered to
video_generation/image_to_video capabilities, prompt + negative prompt,
size/frames/fps/steps/guidance/seed knobs (0 = model default), optional
conditioning-image upload (image-to-video), elapsed-time spinner for
the minutes-long blocking call, inline <video> playback + download.
Calls POST /v1/videos/sync (multipart); resolution rides as both
width/height and size so either upstream convention honors it.
Config side: "video" joins the valid capability modalities, and
/v1/models maps text->video to video_generation and image->video to
image_to_video, mirroring the image mappings. Declare e.g.:
videogen-wan22-5b:
capabilities:
in: [text, image]
out: [video]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXQxVhXBw8PwFAtsVrXSmj
This commit is contained in:
@@ -14,6 +14,7 @@ var validModalities = map[string]struct{}{
|
||||
"text": {},
|
||||
"audio": {},
|
||||
"image": {},
|
||||
"video": {},
|
||||
}
|
||||
|
||||
// ModelCapConfig defines what modalities and features a model supports.
|
||||
@@ -37,12 +38,12 @@ func (c ModelCapConfig) Empty() bool {
|
||||
func (c ModelCapConfig) Validate() error {
|
||||
for _, m := range c.In {
|
||||
if _, ok := validModalities[m]; !ok {
|
||||
return fmt.Errorf("capabilities.in: invalid modality %q, must be one of: text, audio, image", m)
|
||||
return fmt.Errorf("capabilities.in: invalid modality %q, must be one of: text, audio, image, video", m)
|
||||
}
|
||||
}
|
||||
for _, m := range c.Out {
|
||||
if _, ok := validModalities[m]; !ok {
|
||||
return fmt.Errorf("capabilities.out: invalid modality %q, must be one of: text, audio, image", m)
|
||||
return fmt.Errorf("capabilities.out: invalid modality %q, must be one of: text, audio, image, video", m)
|
||||
}
|
||||
}
|
||||
if c.Context < 0 {
|
||||
|
||||
@@ -296,20 +296,25 @@ func TestConfig_ModelCapabilities_Validate(t *testing.T) {
|
||||
assert.NoError(t, caps.Validate())
|
||||
})
|
||||
|
||||
t.Run("valid_video_modality", func(t *testing.T) {
|
||||
caps := ModelCapConfig{In: []string{"text", "image"}, Out: []string{"video"}}
|
||||
assert.NoError(t, caps.Validate())
|
||||
})
|
||||
|
||||
t.Run("invalid_in_modality", func(t *testing.T) {
|
||||
caps := ModelCapConfig{In: []string{"video"}}
|
||||
caps := ModelCapConfig{In: []string{"smell"}}
|
||||
err := caps.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "capabilities.in")
|
||||
assert.Contains(t, err.Error(), "video")
|
||||
assert.Contains(t, err.Error(), "smell")
|
||||
})
|
||||
|
||||
t.Run("invalid_out_modality", func(t *testing.T) {
|
||||
caps := ModelCapConfig{Out: []string{"video"}}
|
||||
caps := ModelCapConfig{Out: []string{"smell"}}
|
||||
err := caps.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "capabilities.out")
|
||||
assert.Contains(t, err.Error(), "video")
|
||||
assert.Contains(t, err.Error(), "smell")
|
||||
})
|
||||
|
||||
t.Run("negative_context", func(t *testing.T) {
|
||||
@@ -327,10 +332,10 @@ models:
|
||||
capabilities:
|
||||
in:
|
||||
- text
|
||||
- video
|
||||
- smell
|
||||
`
|
||||
_, err := LoadConfigFromReader(strings.NewReader(content))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "video")
|
||||
assert.Contains(t, err.Error(), "smell")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,6 +88,12 @@ func renderCapabilities(caps config.ModelCapConfig) (arch map[string]any, capsMa
|
||||
if contains(caps.In, "image") && contains(caps.Out, "image") {
|
||||
capsMap["image_to_image"] = true
|
||||
}
|
||||
if contains(caps.In, "text") && contains(caps.Out, "video") {
|
||||
capsMap["video_generation"] = true
|
||||
}
|
||||
if contains(caps.In, "image") && contains(caps.Out, "video") {
|
||||
capsMap["image_to_video"] = true
|
||||
}
|
||||
}
|
||||
|
||||
if caps.Tools {
|
||||
|
||||
Reference in New Issue
Block a user