kie-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/agents/project-environment-setup.md +149 -0
- data/.claude/commands/soba/implement.md +93 -0
- data/.claude/commands/soba/plan.md +98 -0
- data/.claude/commands/soba/review.md +91 -0
- data/.claude/commands/soba/revise.md +79 -0
- data/.devcontainer/LICENSE +21 -0
- data/.devcontainer/README.md +85 -0
- data/.devcontainer/bin/devcontainer-common.sh +36 -0
- data/.devcontainer/bin/down +55 -0
- data/.devcontainer/bin/init +159 -0
- data/.devcontainer/bin/rebuild +10 -0
- data/.devcontainer/bin/up +11 -0
- data/.devcontainer/compose.yaml +12 -0
- data/.devcontainer/compose.yaml.template +8 -0
- data/.devcontainer/devcontainer.json +30 -0
- data/.devcontainer/devcontainer.local.json +3 -0
- data/.devcontainer/scripts/setup.sh +12 -0
- data/.lefthook/rubocop-autofix.sh +51 -0
- data/.soba/config.yml +78 -0
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +20 -0
- data/LICENSE +21 -0
- data/README.md +310 -0
- data/Rakefile +12 -0
- data/docs/business/INDEX.md +3 -0
- data/docs/business/overview.md +35 -0
- data/docs/development/INDEX.md +3 -0
- data/docs/development/technical-design.md +77 -0
- data/docs/document_system.md +58 -0
- data/lefthook.yml +8 -0
- data/lib/kie/client.rb +36 -0
- data/lib/kie/errors.rb +43 -0
- data/lib/kie/general_engine.rb +56 -0
- data/lib/kie/general_task.rb +106 -0
- data/lib/kie/middleware/raise_error.rb +46 -0
- data/lib/kie/model_definition.rb +10 -0
- data/lib/kie/model_registry.rb +36 -0
- data/lib/kie/models/nano_banana_pro.rb +17 -0
- data/lib/kie/models/suno_v4.rb +14 -0
- data/lib/kie/models/suno_v4_5.rb +14 -0
- data/lib/kie/models/suno_v4_5_all.rb +14 -0
- data/lib/kie/models/suno_v4_5_plus.rb +14 -0
- data/lib/kie/models/suno_v5.rb +14 -0
- data/lib/kie/suno_engine.rb +129 -0
- data/lib/kie/suno_task.rb +99 -0
- data/lib/kie/task.rb +69 -0
- data/lib/kie/version.rb +5 -0
- data/lib/kie.rb +26 -0
- data/sig/kie.rbs +6 -0
- metadata +110 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Kie
|
|
6
|
+
# Represents an asynchronous task for General API endpoints
|
|
7
|
+
class GeneralTask
|
|
8
|
+
include Task
|
|
9
|
+
|
|
10
|
+
RECORD_INFO_ENDPOINT = "/api/v1/jobs/recordInfo"
|
|
11
|
+
|
|
12
|
+
# Status mapping from General API raw states to normalized statuses
|
|
13
|
+
STATUS_MAP = {
|
|
14
|
+
waiting: :processing,
|
|
15
|
+
generating: :processing,
|
|
16
|
+
success: :success,
|
|
17
|
+
fail: :failed
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :task_id, :raw_status, :cost_time, :fail_code, :fail_message
|
|
21
|
+
|
|
22
|
+
alias id task_id
|
|
23
|
+
|
|
24
|
+
def initialize(client:, task_id:)
|
|
25
|
+
@client = client
|
|
26
|
+
@task_id = task_id
|
|
27
|
+
@raw_status = nil
|
|
28
|
+
@result_urls = []
|
|
29
|
+
@cost_time = nil
|
|
30
|
+
@fail_code = nil
|
|
31
|
+
@fail_message = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def refresh!
|
|
35
|
+
response = @client.connection.get(RECORD_INFO_ENDPOINT, taskId: @task_id)
|
|
36
|
+
handle_response(response)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns normalized status (:processing, :success, :failed)
|
|
41
|
+
def status
|
|
42
|
+
return :processing if @raw_status.nil?
|
|
43
|
+
|
|
44
|
+
STATUS_MAP.fetch(@raw_status, :processing)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns array of result URLs
|
|
48
|
+
def urls
|
|
49
|
+
@result_urls
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Alias for backwards compatibility
|
|
53
|
+
alias result_urls urls
|
|
54
|
+
|
|
55
|
+
# Returns the error message from the failed task, or nil if not failed
|
|
56
|
+
def error_message
|
|
57
|
+
@fail_message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns true when task is in pending/waiting state (not yet processing)
|
|
61
|
+
def pending?
|
|
62
|
+
@raw_status.nil? || @raw_status == :waiting
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Raises the appropriate exception based on the failure reason
|
|
66
|
+
def raise_task_error!
|
|
67
|
+
error_class = @fail_code == "CONTENT_POLICY" ? ContentPolicyError : TaskFailedError
|
|
68
|
+
raise error_class.new(@fail_message, fail_code: @fail_code)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def handle_response(response)
|
|
74
|
+
data = JSON.parse(response.body)
|
|
75
|
+
handle_api_error(data) unless data["code"] == 200
|
|
76
|
+
|
|
77
|
+
update_from_data(data["data"])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_api_error(data)
|
|
81
|
+
case data["code"]
|
|
82
|
+
when 404
|
|
83
|
+
raise NotFoundError.new(data["msg"], status_code: data["code"], response_body: data.to_json)
|
|
84
|
+
else
|
|
85
|
+
raise ApiError.new(data["msg"], status_code: data["code"], response_body: data.to_json)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_from_data(data)
|
|
90
|
+
@raw_status = data["state"]&.to_sym
|
|
91
|
+
@cost_time = data["costTime"]
|
|
92
|
+
@fail_code = data["failCode"]
|
|
93
|
+
@fail_message = data["failMsg"]
|
|
94
|
+
parse_result_json(data["resultJson"])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_result_json(result_json)
|
|
98
|
+
return if result_json.nil? || result_json.empty?
|
|
99
|
+
|
|
100
|
+
parsed = JSON.parse(result_json)
|
|
101
|
+
@result_urls = parsed["resultUrls"] || []
|
|
102
|
+
rescue JSON::ParserError
|
|
103
|
+
@result_urls = []
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "faraday"
|
|
5
|
+
|
|
6
|
+
module Kie
|
|
7
|
+
module Middleware
|
|
8
|
+
# Faraday middleware that raises Kie-specific exceptions based on HTTP status codes
|
|
9
|
+
class RaiseError < Faraday::Middleware
|
|
10
|
+
ERROR_MAP = {
|
|
11
|
+
400 => InvalidParametersError,
|
|
12
|
+
401 => AuthenticationError,
|
|
13
|
+
402 => InsufficientCreditsError,
|
|
14
|
+
404 => NotFoundError,
|
|
15
|
+
422 => InvalidParametersError,
|
|
16
|
+
429 => RateLimitError,
|
|
17
|
+
455 => ServiceUnavailableError
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def on_complete(env)
|
|
21
|
+
return if env.success?
|
|
22
|
+
|
|
23
|
+
error_class = determine_error_class(env.status)
|
|
24
|
+
message = extract_message(env.body, env.status)
|
|
25
|
+
|
|
26
|
+
raise error_class.new(message, status_code: env.status, response_body: env.body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def determine_error_class(status)
|
|
32
|
+
return ERROR_MAP[status] if ERROR_MAP.key?(status)
|
|
33
|
+
return ServerError if status >= 500
|
|
34
|
+
|
|
35
|
+
ApiError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extract_message(body, status)
|
|
39
|
+
data = JSON.parse(body)
|
|
40
|
+
data["msg"] || "HTTP #{status}"
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
"HTTP #{status}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kie
|
|
4
|
+
# Central registry for model definitions
|
|
5
|
+
class ModelRegistry
|
|
6
|
+
class << self
|
|
7
|
+
def register(definition)
|
|
8
|
+
registry[definition.name] = definition
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find(model_name)
|
|
12
|
+
registry.fetch(model_name) do
|
|
13
|
+
raise UnknownModelError, "Unknown model: #{model_name}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def registered?(model_name)
|
|
18
|
+
registry.key?(model_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def all
|
|
22
|
+
registry.values
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def clear
|
|
26
|
+
@registry = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def registry
|
|
32
|
+
@registry ||= {}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kie::ModelRegistry.register(
|
|
4
|
+
Kie::ModelDefinition.new(
|
|
5
|
+
name: "nano-banana-pro",
|
|
6
|
+
category: :general,
|
|
7
|
+
input_constraints: {
|
|
8
|
+
prompt: { type: :string, max_length: 20_000, required: true },
|
|
9
|
+
images: { type: :array, max_items: 8, max_file_size: 30_000_000, formats: %w[jpeg png webp], required: false }
|
|
10
|
+
},
|
|
11
|
+
parameters: {
|
|
12
|
+
aspect_ratio: { type: :enum, values: %w[1:1 2:3 3:2 3:4 4:3 4:5 5:4 9:16 16:9 21:9 auto], default: "1:1" },
|
|
13
|
+
resolution: { type: :enum, values: %w[1K 2K 4K], default: "1K" },
|
|
14
|
+
output_format: { type: :enum, values: %w[png jpg], default: "png" }
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kie::ModelRegistry.register(
|
|
4
|
+
Kie::ModelDefinition.new(
|
|
5
|
+
name: "V4",
|
|
6
|
+
category: :suno,
|
|
7
|
+
input_constraints: {
|
|
8
|
+
prompt: { type: :string, max_length: 3000, max_length_non_custom: 500 },
|
|
9
|
+
style: { type: :string, max_length: 200 },
|
|
10
|
+
title: { type: :string, max_length: 80 }
|
|
11
|
+
},
|
|
12
|
+
parameters: {}
|
|
13
|
+
)
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kie::ModelRegistry.register(
|
|
4
|
+
Kie::ModelDefinition.new(
|
|
5
|
+
name: "V4_5",
|
|
6
|
+
category: :suno,
|
|
7
|
+
input_constraints: {
|
|
8
|
+
prompt: { type: :string, max_length: 5000, max_length_non_custom: 500 },
|
|
9
|
+
style: { type: :string, max_length: 1000 },
|
|
10
|
+
title: { type: :string, max_length: 80 }
|
|
11
|
+
},
|
|
12
|
+
parameters: {}
|
|
13
|
+
)
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kie::ModelRegistry.register(
|
|
4
|
+
Kie::ModelDefinition.new(
|
|
5
|
+
name: "V4_5ALL",
|
|
6
|
+
category: :suno,
|
|
7
|
+
input_constraints: {
|
|
8
|
+
prompt: { type: :string, max_length: 5000, max_length_non_custom: 500 },
|
|
9
|
+
style: { type: :string, max_length: 1000 },
|
|
10
|
+
title: { type: :string, max_length: 80 }
|
|
11
|
+
},
|
|
12
|
+
parameters: {}
|
|
13
|
+
)
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kie::ModelRegistry.register(
|
|
4
|
+
Kie::ModelDefinition.new(
|
|
5
|
+
name: "V4_5PLUS",
|
|
6
|
+
category: :suno,
|
|
7
|
+
input_constraints: {
|
|
8
|
+
prompt: { type: :string, max_length: 5000, max_length_non_custom: 500 },
|
|
9
|
+
style: { type: :string, max_length: 1000 },
|
|
10
|
+
title: { type: :string, max_length: 80 }
|
|
11
|
+
},
|
|
12
|
+
parameters: {}
|
|
13
|
+
)
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kie::ModelRegistry.register(
|
|
4
|
+
Kie::ModelDefinition.new(
|
|
5
|
+
name: "V5",
|
|
6
|
+
category: :suno,
|
|
7
|
+
input_constraints: {
|
|
8
|
+
prompt: { type: :string, max_length: 5000, max_length_non_custom: 500 },
|
|
9
|
+
style: { type: :string, max_length: 1000 },
|
|
10
|
+
title: { type: :string, max_length: 80 }
|
|
11
|
+
},
|
|
12
|
+
parameters: {}
|
|
13
|
+
)
|
|
14
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Kie
|
|
6
|
+
# Engine for Suno API endpoints (/api/v1/generate) - Suno music generation
|
|
7
|
+
class SunoEngine
|
|
8
|
+
CREATE_TASK_ENDPOINT = "/api/v1/generate"
|
|
9
|
+
|
|
10
|
+
OPTIONAL_FIELD_MAPPING = {
|
|
11
|
+
callback_url: :callBackUrl,
|
|
12
|
+
negative_tags: :negativeTags,
|
|
13
|
+
vocal_gender: :vocalGender,
|
|
14
|
+
style_weight: :styleWeight,
|
|
15
|
+
weirdness_constraint: :weirdnessConstraint,
|
|
16
|
+
audio_weight: :audioWeight,
|
|
17
|
+
persona_id: :personaId
|
|
18
|
+
}.freeze
|
|
19
|
+
private_constant :OPTIONAL_FIELD_MAPPING
|
|
20
|
+
|
|
21
|
+
def initialize(client:)
|
|
22
|
+
@client = client
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate(model:, input:, **)
|
|
26
|
+
definition = ModelRegistry.find(model)
|
|
27
|
+
validate_input!(definition, input)
|
|
28
|
+
|
|
29
|
+
response = @client.connection.post(CREATE_TASK_ENDPOINT) do |req|
|
|
30
|
+
req.body = build_request_body(definition, input)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
handle_response(response)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def validate_input!(definition, input)
|
|
39
|
+
constraints = definition.input_constraints
|
|
40
|
+
custom_mode = input[:custom_mode] || false
|
|
41
|
+
|
|
42
|
+
validate_prompt!(constraints, input, custom_mode)
|
|
43
|
+
validate_style!(constraints, input) if custom_mode && input[:style]
|
|
44
|
+
validate_title!(constraints, input) if custom_mode && input[:title]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_prompt!(constraints, input, custom_mode)
|
|
48
|
+
prompt = input[:prompt]
|
|
49
|
+
return unless prompt && constraints[:prompt]
|
|
50
|
+
|
|
51
|
+
max_length = if custom_mode
|
|
52
|
+
constraints[:prompt][:max_length]
|
|
53
|
+
else
|
|
54
|
+
constraints[:prompt][:max_length_non_custom] || constraints[:prompt][:max_length]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
return unless prompt.length > max_length
|
|
58
|
+
|
|
59
|
+
raise ValidationError, "prompt exceeds maximum length of #{max_length} characters"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_style!(constraints, input)
|
|
63
|
+
style = input[:style]
|
|
64
|
+
return unless style && constraints[:style]
|
|
65
|
+
|
|
66
|
+
max_length = constraints[:style][:max_length]
|
|
67
|
+
return unless style.length > max_length
|
|
68
|
+
|
|
69
|
+
raise ValidationError, "style exceeds maximum length of #{max_length} characters"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_title!(constraints, input)
|
|
73
|
+
title = input[:title]
|
|
74
|
+
return unless title && constraints[:title]
|
|
75
|
+
|
|
76
|
+
max_length = constraints[:title][:max_length]
|
|
77
|
+
return unless title.length > max_length
|
|
78
|
+
|
|
79
|
+
raise ValidationError, "title exceeds maximum length of #{max_length} characters"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_request_body(definition, input)
|
|
83
|
+
body = {
|
|
84
|
+
model: definition.name,
|
|
85
|
+
customMode: input[:custom_mode] || false,
|
|
86
|
+
instrumental: input[:instrumental] || false,
|
|
87
|
+
prompt: input[:prompt],
|
|
88
|
+
callBackUrl: input[:callback_url] || "https://example.invalid/callback"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
add_custom_mode_fields(body, input) if input[:custom_mode]
|
|
92
|
+
add_optional_fields(body, input)
|
|
93
|
+
|
|
94
|
+
body.to_json
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_custom_mode_fields(body, input)
|
|
98
|
+
body[:style] = input[:style] if input[:style]
|
|
99
|
+
body[:title] = input[:title] if input[:title]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_optional_fields(body, input)
|
|
103
|
+
OPTIONAL_FIELD_MAPPING.each do |input_key, api_key|
|
|
104
|
+
body[api_key] = input[input_key] if input[input_key]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_response(response)
|
|
109
|
+
data = JSON.parse(response.body)
|
|
110
|
+
handle_api_error(data) unless data["code"] == 200
|
|
111
|
+
|
|
112
|
+
SunoTask.new(client: @client, task_id: data["data"]["taskId"])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def handle_api_error(data)
|
|
116
|
+
error_class = error_class_for_code(data["code"])
|
|
117
|
+
raise error_class.new(data["msg"], status_code: data["code"], response_body: data.to_json)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def error_class_for_code(code)
|
|
121
|
+
case code
|
|
122
|
+
when 400, 422 then InvalidParametersError
|
|
123
|
+
when 402 then InsufficientCreditsError
|
|
124
|
+
when 429 then RateLimitError
|
|
125
|
+
else ApiError
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Kie
|
|
6
|
+
# Represents an asynchronous task for Suno API endpoints (Suno music generation)
|
|
7
|
+
class SunoTask
|
|
8
|
+
include Task
|
|
9
|
+
|
|
10
|
+
RECORD_INFO_ENDPOINT = "/api/v1/generate/record-info"
|
|
11
|
+
|
|
12
|
+
# Status mapping from Suno API raw states to normalized statuses
|
|
13
|
+
STATUS_MAP = {
|
|
14
|
+
PENDING: :processing,
|
|
15
|
+
TEXT_SUCCESS: :processing,
|
|
16
|
+
FIRST_SUCCESS: :processing,
|
|
17
|
+
SUCCESS: :success,
|
|
18
|
+
CREATE_TASK_FAILED: :failed,
|
|
19
|
+
GENERATE_AUDIO_FAILED: :failed,
|
|
20
|
+
CALLBACK_EXCEPTION: :failed,
|
|
21
|
+
SENSITIVE_WORD_ERROR: :failed
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :task_id, :raw_status, :error_code, :error_message, :suno_data, :audio_urls, :image_urls
|
|
25
|
+
|
|
26
|
+
alias id task_id
|
|
27
|
+
|
|
28
|
+
def initialize(client:, task_id:)
|
|
29
|
+
@client = client
|
|
30
|
+
@task_id = task_id
|
|
31
|
+
@raw_status = nil
|
|
32
|
+
@suno_data = []
|
|
33
|
+
@audio_urls = []
|
|
34
|
+
@image_urls = []
|
|
35
|
+
@error_code = nil
|
|
36
|
+
@error_message = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def refresh!
|
|
40
|
+
response = @client.connection.get(RECORD_INFO_ENDPOINT, taskId: @task_id)
|
|
41
|
+
handle_response(response)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns normalized status (:processing, :success, :failed)
|
|
46
|
+
def status
|
|
47
|
+
return :processing if @raw_status.nil?
|
|
48
|
+
|
|
49
|
+
STATUS_MAP.fetch(@raw_status, :processing)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns array of audio URLs (implements Task interface)
|
|
53
|
+
def urls
|
|
54
|
+
@audio_urls
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Raises the appropriate exception based on the failure reason
|
|
58
|
+
def raise_task_error!
|
|
59
|
+
error_class = @raw_status == :SENSITIVE_WORD_ERROR ? ContentPolicyError : TaskFailedError
|
|
60
|
+
raise error_class.new(@error_message, fail_code: @raw_status.to_s)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def handle_response(response)
|
|
66
|
+
data = JSON.parse(response.body)
|
|
67
|
+
handle_api_error(data) unless data["code"] == 200
|
|
68
|
+
|
|
69
|
+
update_from_data(data["data"])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def handle_api_error(data)
|
|
73
|
+
case data["code"]
|
|
74
|
+
when 404
|
|
75
|
+
raise NotFoundError.new(data["msg"], status_code: data["code"], response_body: data.to_json)
|
|
76
|
+
else
|
|
77
|
+
raise ApiError.new(data["msg"], status_code: data["code"], response_body: data.to_json)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update_from_data(data)
|
|
82
|
+
@raw_status = data["status"]&.to_sym
|
|
83
|
+
@error_code = data["errorCode"]
|
|
84
|
+
@error_message = data["errorMessage"]
|
|
85
|
+
parse_response(data["response"])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_response(response)
|
|
89
|
+
return if response.nil?
|
|
90
|
+
|
|
91
|
+
suno_data = response["sunoData"]
|
|
92
|
+
return if suno_data.nil? || suno_data.empty?
|
|
93
|
+
|
|
94
|
+
@suno_data = suno_data
|
|
95
|
+
@audio_urls = suno_data.filter_map { |item| item["audioUrl"] }
|
|
96
|
+
@image_urls = suno_data.filter_map { |item| item["imageUrl"] }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/kie/task.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kie
|
|
4
|
+
# Common interface module for all task types (General API, Suno API, etc.)
|
|
5
|
+
# Classes including this module must implement:
|
|
6
|
+
# - #status: Returns normalized status symbol (:processing, :success, :failed)
|
|
7
|
+
# - #urls: Returns array of result URLs
|
|
8
|
+
module Task
|
|
9
|
+
NORMALIZED_STATUSES = %i[processing success failed].freeze
|
|
10
|
+
|
|
11
|
+
def status
|
|
12
|
+
raise NotImplementedError, "#{self.class} must implement #status"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def urls
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #urls"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def error_message
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #error_message"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def completed?
|
|
24
|
+
success? || failed?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def success?
|
|
28
|
+
status == :success
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failed?
|
|
32
|
+
status == :failed
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def processing?
|
|
36
|
+
status == :processing
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Waits for the task to complete by polling refresh!
|
|
40
|
+
# @param timeout [Numeric] Maximum time to wait in seconds (default: 300)
|
|
41
|
+
# @param interval [Numeric] Time between polling attempts in seconds (default: 2)
|
|
42
|
+
# @param raise_on_failure [Boolean] Whether to raise an exception on task failure (default: true)
|
|
43
|
+
# @return [self] Returns the task instance for chaining
|
|
44
|
+
# @raise [TimeoutError] if the timeout is exceeded
|
|
45
|
+
# @raise [TaskFailedError, ContentPolicyError] if the task fails and raise_on_failure is true
|
|
46
|
+
def wait(timeout: 300, interval: 2, raise_on_failure: true)
|
|
47
|
+
deadline = Time.now + timeout
|
|
48
|
+
poll_until_completed(deadline: deadline, interval: interval, raise_on_failure: raise_on_failure)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def poll_until_completed(deadline:, interval:, raise_on_failure:)
|
|
54
|
+
loop do
|
|
55
|
+
refresh!
|
|
56
|
+
return self if success?
|
|
57
|
+
return handle_failure(raise_on_failure) if failed?
|
|
58
|
+
raise TimeoutError, "Task did not complete within timeout" if Time.now > deadline
|
|
59
|
+
|
|
60
|
+
sleep interval
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def handle_failure(raise_on_failure)
|
|
65
|
+
raise_task_error! if raise_on_failure
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/kie/version.rb
ADDED
data/lib/kie.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "kie/version"
|
|
4
|
+
|
|
5
|
+
module Kie
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "kie/errors"
|
|
10
|
+
require_relative "kie/middleware/raise_error"
|
|
11
|
+
require_relative "kie/task"
|
|
12
|
+
require_relative "kie/model_definition"
|
|
13
|
+
require_relative "kie/model_registry"
|
|
14
|
+
require_relative "kie/client"
|
|
15
|
+
require_relative "kie/general_task"
|
|
16
|
+
require_relative "kie/general_engine"
|
|
17
|
+
require_relative "kie/suno_task"
|
|
18
|
+
require_relative "kie/suno_engine"
|
|
19
|
+
|
|
20
|
+
# Model definitions
|
|
21
|
+
require_relative "kie/models/nano_banana_pro"
|
|
22
|
+
require_relative "kie/models/suno_v4"
|
|
23
|
+
require_relative "kie/models/suno_v4_5"
|
|
24
|
+
require_relative "kie/models/suno_v4_5_plus"
|
|
25
|
+
require_relative "kie/models/suno_v4_5_all"
|
|
26
|
+
require_relative "kie/models/suno_v5"
|