Files
geekai/api/service/video/adapters/keling_adapter.go
T
RockYang 9ccff4efbc feat(release): migrate GeekAI v4.3.0 to open source
- Sync backend and frontend from GeekAI Plus v4.3.0

- Remove commercial License flows and update open-source deployment defaults

- Preserve Docker Compose deployment and bump image tags to v4.3.0

BREAKING CHANGE: commercial License configuration and related endpoints are removed
2026-08-11 14:51:20 +08:00

268 lines
7.8 KiB
Go

package adapters
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"io"
"net/http"
"time"
"gorm.io/gorm"
)
// KelingAdapter 可灵视频生成适配器
type KelingAdapter struct {
db *gorm.DB
}
// NewKelingAdapter 创建可灵适配器
func NewKelingAdapter(db *gorm.DB) *KelingAdapter {
return &KelingAdapter{
db: db,
}
}
// GetProvider 获取服务提供商名称
func (a *KelingAdapter) GetProvider() string {
return "keling"
}
// KelingCreateRequest 可灵创建任务请求
type KelingCreateRequest struct {
ModelName string `json:"model_name"`
Prompt string `json:"prompt"`
NegativePrompt string `json:"negative_prompt,omitempty"`
CfgScale float64 `json:"cfg_scale,omitempty"`
Mode string `json:"mode,omitempty"`
AspectRatio string `json:"aspect_ratio,omitempty"`
Duration string `json:"duration,omitempty"`
Sound bool `json:"sound,omitempty"`
Image string `json:"image,omitempty"`
ImageTail string `json:"image_tail,omitempty"`
}
// KelingCreateResponse 可灵创建任务响应
type KelingCreateResponse struct {
Code int `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
Data struct {
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
} `json:"data"`
}
// KelingQueryResponse 可灵查询任务响应
type KelingQueryResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
TaskStatusMsg string `json:"task_status_msg"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
TaskResult struct {
Images []struct {
Index int `json:"index"`
URL string `json:"url"`
} `json:"images,omitempty"`
Videos []struct {
ID string `json:"id"`
URL string `json:"url"`
Duration string `json:"duration"`
} `json:"videos,omitempty"`
} `json:"task_result"`
} `json:"data"`
}
// CreateTask 创建视频生成任务
func (a *KelingAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) {
// 解析任务参数
paramsMap, ok := task.Params.(map[string]interface{})
if !ok {
return CreateTaskResponse{}, errors.New("invalid params type for KeLing video task")
}
// 构建请求参数
payload := KelingCreateRequest{
Prompt: task.Prompt,
}
// 从 params 中提取参数
if modelName, ok := paramsMap["model_name"].(string); ok {
payload.ModelName = modelName
}
if prompt, ok := paramsMap["prompt"].(string); ok {
payload.Prompt = prompt
}
if negativePrompt, ok := paramsMap["negative_prompt"].(string); ok {
payload.NegativePrompt = negativePrompt
}
if cfgScale, ok := paramsMap["cfg_scale"].(float64); ok {
payload.CfgScale = cfgScale
}
if mode, ok := paramsMap["mode"].(string); ok {
payload.Mode = mode
}
if aspectRatio, ok := paramsMap["aspect_ratio"].(string); ok {
payload.AspectRatio = aspectRatio
}
if duration, ok := paramsMap["duration"].(string); ok {
payload.Duration = duration
}
if sound, ok := paramsMap["sound"].(bool); ok {
payload.Sound = sound
}
// 处理图生视频
taskType, ok := paramsMap["task_type"].(string)
if ok && taskType == "image2video" {
if image, ok := paramsMap["image"].(string); ok {
payload.Image = image
}
if imageTail, ok := paramsMap["image_tail"].(string); ok {
payload.ImageTail = imageTail
}
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
return CreateTaskResponse{}, fmt.Errorf("failed to marshal payload: %v", err)
}
logger.Debugf("KelingCreateRequest: %+v", string(jsonPayload))
// 发送请求
url := fmt.Sprintf("%s/kling/v1/videos/%s", videoConfig.ApiURL, taskType)
req, err := http.NewRequest("POST", url, bytes.NewReader(jsonPayload))
if err != nil {
return CreateTaskResponse{}, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey)
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{Timeout: time.Duration(30) * time.Second}
resp, err := client.Do(req)
if err != nil {
return CreateTaskResponse{}, fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()
// 处理响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return CreateTaskResponse{}, fmt.Errorf("failed to read response: %v", err)
}
if resp.StatusCode != http.StatusOK {
return CreateTaskResponse{}, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
var apiResponse KelingCreateResponse
if err := json.Unmarshal(body, &apiResponse); err != nil {
return CreateTaskResponse{}, fmt.Errorf("failed to parse response: %v", err)
}
if apiResponse.Code != 0 {
return CreateTaskResponse{}, fmt.Errorf("API error: %s", apiResponse.Message)
}
return CreateTaskResponse{
TaskId: apiResponse.Data.TaskID,
Channel: videoConfig.ApiURL,
Prompt: task.Prompt,
State: types.VideoStatusPending,
CreatedAt: time.Now().Format(time.RFC3339),
}, nil
}
// QueryTask 查询任务状态
func (a *KelingAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) {
// 从 taskId 中提取 action(可灵的 taskId 格式可能包含 action 信息)
// 这里需要从任务信息中获取 task_type,暂时使用 text2video 作为默认值
action := "text2video"
// 尝试从 channel 或其他地方获取 action,这里简化处理
// 实际应该从任务信息中获取
url := fmt.Sprintf("%s/kling/v1/videos/%s/%s", videoConfig.ApiURL, action, taskId)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return QueryTaskResponse{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: time.Duration(30) * time.Second}
res, err := client.Do(req)
if err != nil {
return QueryTaskResponse{}, fmt.Errorf("failed to execute request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
return QueryTaskResponse{}, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, string(body))
}
body, err := io.ReadAll(res.Body)
if err != nil {
return QueryTaskResponse{}, fmt.Errorf("failed to read response body: %w", err)
}
var response KelingQueryResponse
if err := json.Unmarshal(body, &response); err != nil {
return QueryTaskResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
}
if response.Code != 0 {
return QueryTaskResponse{}, fmt.Errorf("API error: %s", response.Message)
}
// 转换状态
state := response.Data.TaskStatus
status := state
switch state {
case "in_progress", "processing":
status = types.VideoStatusInProgress
case "completed", "succeed", "success":
status = types.VideoStatusSuccess
case "failed":
status = types.VideoStatusFailed
default:
status = types.VideoStatusPending
}
// 构建响应
result := QueryTaskResponse{
TaskId: response.Data.TaskID,
Status: status,
ErrMsg: response.Data.TaskStatusMsg,
StatusMsg: response.Data.TaskStatusMsg,
Output: string(body),
}
// 提取视频URL
if len(response.Data.TaskResult.Videos) > 0 {
result.VideoURL = response.Data.TaskResult.Videos[0].URL
}
return result, nil
}