Support start-end frame image2video routing
The CLI now submits gateway-compatible first/final-frame payloads, so the server accepts the same public SkillHub schema fields, normalizes them to images[0]/images[1] with metadata.action=firstTailGenerate, and routes multi-frame Vidu-style submissions through the unified video generations endpoint. Constraint: Align server behavior with popiartcli v0.3.21 start/end-frame payloads Constraint: Keep existing single-image image2video and MiniMax paths compatible Rejected: Hardcode only the new fields in server seed skills | loading SkillHub input_schema.json keeps the remote catalog as the source of truth Confidence: medium Scope-risk: moderate Directive: Preserve first-frame then final-frame ordering when changing video reference handling Tested: go test ./... Tested: make build Tested: GOOS=linux GOARCH=amd64 go build -o dist/popiartserver-linux-amd64 ./cmd/popiartserver Not-tested: Live test-server deployment; current SSH key is rejected by 101.42.99.35
This commit is contained in:
+64
-15
@@ -62,6 +62,18 @@ func loadSkills(skillhubDir string) ([]skill, error) {
|
||||
tags := tagsFromSkillID(entry.Name)
|
||||
name := defaultString(strings.TrimSpace(doc.Title), displayNameFromSkillID(entry.Name))
|
||||
description := strings.TrimSpace(doc.Description)
|
||||
inputSchema := inputSchemaForRoute(routeKey)
|
||||
if schema, ok, err := readSkillSchemaFile(filepath.Join(skillhubDir, skillPath, "input_schema.json")); err != nil {
|
||||
return nil, fmt.Errorf("read input schema for %s: %w", entry.Name, err)
|
||||
} else if ok {
|
||||
inputSchema = schema
|
||||
}
|
||||
outputSchema := defaultOutputSchema()
|
||||
if schema, ok, err := readSkillSchemaFile(filepath.Join(skillhubDir, skillPath, "output_schema.json")); err != nil {
|
||||
return nil, fmt.Errorf("read output schema for %s: %w", entry.Name, err)
|
||||
} else if ok {
|
||||
outputSchema = schema
|
||||
}
|
||||
|
||||
items = append(items, skill{
|
||||
ID: defaultString(strings.TrimSpace(doc.Name), entry.Name),
|
||||
@@ -72,8 +84,8 @@ func loadSkills(skillhubDir string) ([]skill, error) {
|
||||
ModelType: category,
|
||||
RouteKey: routeKey,
|
||||
EstimatedDurationS: estimateDuration(routeKey),
|
||||
InputSchema: inputSchemaForRoute(routeKey),
|
||||
OutputSchema: defaultOutputSchema(),
|
||||
InputSchema: inputSchema,
|
||||
OutputSchema: outputSchema,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,6 +95,24 @@ func loadSkills(skillhubDir string) ([]skill, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func readSkillSchemaFile(path string) (map[string]any, bool, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
var schema map[string]any
|
||||
if err := json.Unmarshal(data, &schema); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(schema) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
return schema, true, nil
|
||||
}
|
||||
|
||||
type skillDoc struct {
|
||||
Name string
|
||||
Description string
|
||||
@@ -249,19 +279,7 @@ func inputSchemaForRoute(routeKey string) map[string]any {
|
||||
},
|
||||
}
|
||||
case "video.image2video":
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"source_artifact_id": map[string]any{"type": "string"},
|
||||
"image_url": map[string]any{"type": "string"},
|
||||
"prompt": map[string]any{"type": "string"},
|
||||
"motion_prompt": map[string]any{"type": "string"},
|
||||
"duration_s": map[string]any{"type": "integer"},
|
||||
"camera_motion": map[string]any{"type": "string"},
|
||||
"mood": map[string]any{"type": "string"},
|
||||
"aspect_ratio": map[string]any{"type": "string"},
|
||||
},
|
||||
}
|
||||
return imageToVideoInputSchema()
|
||||
default:
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
@@ -275,3 +293,34 @@ func inputSchemaForRoute(routeKey string) map[string]any {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func imageToVideoInputSchema() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"source_artifact_id": map[string]any{"type": "string"},
|
||||
"image_url": map[string]any{"type": "string"},
|
||||
"reference_image_url": map[string]any{"type": "string"},
|
||||
"last_frame_image_url": map[string]any{"type": "string"},
|
||||
"end_frame_image_url": map[string]any{"type": "string"},
|
||||
"last_frame_artifact_id": map[string]any{"type": "string"},
|
||||
"end_frame_artifact_id": map[string]any{"type": "string"},
|
||||
"images": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||||
"metadata": map[string]any{"type": "object"},
|
||||
"prompt": map[string]any{"type": "string"},
|
||||
"motion_prompt": map[string]any{"type": "string"},
|
||||
"negative_prompt": map[string]any{"type": "string"},
|
||||
"duration_s": map[string]any{"type": "number"},
|
||||
"seconds": map[string]any{"type": "number"},
|
||||
"fps": map[string]any{"type": "number"},
|
||||
"camera_motion": map[string]any{"type": "string"},
|
||||
"motion_intensity": map[string]any{"type": "string"},
|
||||
"style": map[string]any{"type": "string"},
|
||||
"size": map[string]any{"type": "string"},
|
||||
"mood": map[string]any{"type": "string"},
|
||||
"aspect_ratio": map[string]any{"type": "string"},
|
||||
"seed": map[string]any{"type": "number"},
|
||||
"notes": map[string]any{"type": "string"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user