add stable media support and sync skillhub UI
This commit is contained in:
+105
-2
@@ -30,6 +30,7 @@ type Config struct {
|
||||
NewAPIToken string
|
||||
DefaultImageModel string
|
||||
DefaultVideoModel string
|
||||
PublicBaseURL string
|
||||
DataDir string
|
||||
SQLitePath string
|
||||
SessionSecret string
|
||||
@@ -42,11 +43,16 @@ func ConfigFromEnv() Config {
|
||||
NewAPIToken: strings.TrimSpace(os.Getenv("POPIART_NEWAPI_TOKEN")),
|
||||
DefaultImageModel: strings.TrimSpace(os.Getenv("POPIART_DEFAULT_IMAGE_MODEL")),
|
||||
DefaultVideoModel: strings.TrimSpace(os.Getenv("POPIART_DEFAULT_VIDEO_MODEL")),
|
||||
PublicBaseURL: strings.TrimSpace(os.Getenv("POPIART_PUBLIC_BASE_URL")),
|
||||
DataDir: strings.TrimSpace(os.Getenv("POPIART_DATA_DIR")),
|
||||
SQLitePath: strings.TrimSpace(os.Getenv("POPIART_SQLITE_PATH")),
|
||||
SessionSecret: strings.TrimSpace(os.Getenv("POPIART_SESSION_SECRET")),
|
||||
SkillhubDir: strings.TrimSpace(os.Getenv("POPIART_SKILLHUB_DIR")),
|
||||
}
|
||||
return normalizeConfig(cfg)
|
||||
}
|
||||
|
||||
func normalizeConfig(cfg Config) Config {
|
||||
if cfg.NewAPIBaseURL == "" {
|
||||
cfg.NewAPIBaseURL = "http://127.0.0.1:3000"
|
||||
}
|
||||
@@ -57,7 +63,11 @@ func ConfigFromEnv() Config {
|
||||
cfg.DefaultVideoModel = "viduq2"
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = "./data"
|
||||
if strings.TrimSpace(cfg.SQLitePath) != "" {
|
||||
cfg.DataDir = filepath.Dir(cfg.SQLitePath)
|
||||
} else {
|
||||
cfg.DataDir = "./data"
|
||||
}
|
||||
}
|
||||
if cfg.SQLitePath == "" {
|
||||
cfg.SQLitePath = filepath.Join(cfg.DataDir, "popiart.db")
|
||||
@@ -131,6 +141,7 @@ type imageEditReference struct {
|
||||
Filename string
|
||||
ContentType string
|
||||
Content []byte
|
||||
URL string
|
||||
}
|
||||
|
||||
type openAIVideoResponse struct {
|
||||
@@ -587,7 +598,7 @@ func (c *newAPIClient) generateGeminiImageRefs(ctx context.Context, token, model
|
||||
"generationConfig": generationConfig,
|
||||
}
|
||||
if len(imageConfig) > 0 {
|
||||
payload["imageConfig"] = imageConfig
|
||||
generationConfig["imageConfig"] = imageConfig
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
@@ -629,6 +640,9 @@ func (c *newAPIClient) submitImageToVideoTask(ctx context.Context, token, modelI
|
||||
if prompt == "" {
|
||||
prompt = "Generate a short polished image-to-video clip from the provided reference image."
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelID)), "vidu") && strings.TrimSpace(ref.URL) != "" {
|
||||
return c.submitImageToVideoTaskByURL(ctx, token, modelID, input, ref, prompt)
|
||||
}
|
||||
if len(ref.Content) == 0 {
|
||||
return "", errors.New("reference image content is required")
|
||||
}
|
||||
@@ -713,6 +727,71 @@ func (c *newAPIClient) submitImageToVideoTask(ctx context.Context, token, modelI
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
func (c *newAPIClient) submitImageToVideoTaskByURL(ctx context.Context, token, modelID string, input map[string]any, ref imageEditReference, prompt string) (string, error) {
|
||||
payload := map[string]any{
|
||||
"model": modelID,
|
||||
"prompt": prompt,
|
||||
"images": []string{strings.TrimSpace(ref.URL)},
|
||||
"duration": 5,
|
||||
}
|
||||
if duration := strings.TrimSpace(resolveVideoDurationSeconds(input)); duration != "" {
|
||||
if parsed, err := strconv.Atoi(duration); err == nil && parsed > 0 {
|
||||
payload["duration"] = parsed
|
||||
}
|
||||
}
|
||||
if size := resolveVideoSize(modelID, input, ref); size != "" {
|
||||
payload["size"] = size
|
||||
}
|
||||
if aspectRatio := resolveVideoAspectRatio(input, ref); aspectRatio != "" {
|
||||
payload["metadata"] = map[string]any{
|
||||
"aspect_ratio": aspectRatio,
|
||||
}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/videos", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var decoded openAIVideoResponse
|
||||
if err := json.Unmarshal(respBody, &decoded); err != nil {
|
||||
return "", fmt.Errorf("decode PopiNewAPI video submit response: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", decodeTaskAPIError(respBody, resp.StatusCode)
|
||||
}
|
||||
if decoded.Error != nil && strings.TrimSpace(decoded.Error.Message) != "" {
|
||||
return "", errors.New(decoded.Error.Message)
|
||||
}
|
||||
|
||||
taskID := strings.TrimSpace(decoded.ID)
|
||||
if taskID == "" {
|
||||
taskID = strings.TrimSpace(decoded.TaskID)
|
||||
}
|
||||
if taskID == "" {
|
||||
return "", errors.New("PopiNewAPI returned no task id")
|
||||
}
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
func (c *newAPIClient) fetchVideoTask(ctx context.Context, token, taskID string) (*videoTaskResult, error) {
|
||||
if !c.enabled() {
|
||||
return nil, errors.New("PopiNewAPI base URL is not configured")
|
||||
@@ -997,6 +1076,12 @@ func resolveGeminiImageSize(input map[string]any) string {
|
||||
|
||||
func (c *newAPIClient) openResultRef(ctx context.Context, token string, ref resultRef) (string, int64, io.ReadCloser, error) {
|
||||
switch ref.Kind {
|
||||
case "local_path":
|
||||
file, err := os.Open(ref.LocalPath)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
return defaultString(strings.TrimSpace(ref.ContentType), "application/octet-stream"), ref.SizeBytes, file, nil
|
||||
case "data_url":
|
||||
contentType, content, err := decodeDataURL(ref.DataURL)
|
||||
if err != nil {
|
||||
@@ -1137,6 +1222,24 @@ func resolveVideoSize(modelID string, input map[string]any, ref imageEditReferen
|
||||
return size
|
||||
}
|
||||
|
||||
func resolveVideoAspectRatio(input map[string]any, ref imageEditReference) string {
|
||||
if input != nil {
|
||||
if aspectRatio := strings.TrimSpace(stringValue(input["aspect_ratio"])); aspectRatio != "" {
|
||||
return aspectRatio
|
||||
}
|
||||
}
|
||||
if len(ref.Content) > 0 {
|
||||
cfg, _, err := image.DecodeConfig(bytes.NewReader(ref.Content))
|
||||
if err == nil && cfg.Width > 0 && cfg.Height > 0 {
|
||||
if cfg.Width >= cfg.Height {
|
||||
return "16:9"
|
||||
}
|
||||
return "9:16"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveBaseVideoSize(input map[string]any, ref imageEditReference) string {
|
||||
if input != nil {
|
||||
if size := strings.TrimSpace(stringValue(input["size"])); size != "" {
|
||||
|
||||
Reference in New Issue
Block a user