buble 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9a7f0b94c29ce6c1dac4973776d3963d6c5089930b6401a6a4dfd770bb204860
4
+ data.tar.gz: d5235b3f9911f2f731f9d09902e198887464e7cecd8e1743bd3b2b5df5f3289b
5
+ SHA512:
6
+ metadata.gz: 54f916c30d5162bd18582f01cfd1c33f902d2ea64517e6727dc756621146c8d67a0ff6fe5fd153843df9c84acef6ccc82782ca133ef64965cb26d2f38d91215b
7
+ data.tar.gz: befb6e7610c07676ed1ced3ee4ac6a8e2bc366dd044629536d49a6acf45bb27235e3c37ba8e771ed1ccf85c11985d472b04346af219ea2ef581e72da51258520
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Buble
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # Buble SDK for Ruby
2
+
3
+ Official Ruby SDK for [Buble](https://buble.ai/), built for the [Buble public API](https://buble.ai/docs).
4
+
5
+ Use this SDK from server-side Ruby applications to discover media models, upload source media, create asynchronous image and video generation tasks, run preconfigured Buble app workflows, and call chat models through OpenAI, Anthropic Messages, and Gemini-compatible API formats.
6
+
7
+ Keep API keys on the server. Do not expose `BUBLE_API_KEY` in browser, mobile, or other client-side code.
8
+
9
+ ## Installation
10
+
11
+ After publication to RubyGems:
12
+
13
+ ```bash
14
+ gem install buble
15
+ ```
16
+
17
+ Bundler:
18
+
19
+ ```ruby
20
+ gem "buble"
21
+ ```
22
+
23
+ The gem requires Ruby 3.3+ and has no runtime dependencies outside the Ruby standard library.
24
+
25
+ ## Quick Start
26
+
27
+ Set your API key:
28
+
29
+ ```bash
30
+ export BUBLE_API_KEY="sk_..."
31
+ ```
32
+
33
+ The generation examples below create real Buble generation tasks and may consume credits.
34
+
35
+ ```ruby
36
+ require "buble"
37
+
38
+ client = Buble::Client.new
39
+
40
+ task = client.generations.create(
41
+ model: "google/nano-banana",
42
+ mode: "text_to_image",
43
+ prompt: "A cinematic product photo of a matte black espresso cup",
44
+ aspect_ratio: "1:1",
45
+ output_format: "png"
46
+ )
47
+
48
+ result = client.generations.wait(task.dig("data", "id"))
49
+ puts result.dig("data", "result", "images", 0, "url")
50
+ ```
51
+
52
+ The client reads `BUBLE_API_KEY` and `BUBLE_BASE_URL` from the environment when omitted.
53
+
54
+ ## Configuration
55
+
56
+ ```ruby
57
+ client = Buble::Client.new(
58
+ api_key: "sk_...",
59
+ base_url: "https://buble.ai",
60
+ timeout: 60,
61
+ headers: {
62
+ "X-Request-Id" => "request-id"
63
+ }
64
+ )
65
+ ```
66
+
67
+ ## Discover Media Models
68
+
69
+ ```ruby
70
+ models = client.media_models.list(media_type: "video")
71
+
72
+ models.fetch("data", []).each do |model|
73
+ puts model["model"]
74
+ end
75
+ ```
76
+
77
+ Use media model discovery as the source of truth for model keys, modes, required inputs, and public parameters. New Buble models can become available without an SDK release.
78
+
79
+ ## Upload Files
80
+
81
+ ```ruby
82
+ upload = Buble::FileUpload.from_path("reference.png", content_type: "image/png")
83
+
84
+ uploaded = client.files.upload(
85
+ upload,
86
+ file_type: "image",
87
+ model: "google/nano-banana",
88
+ mode: "image_to_image"
89
+ )
90
+
91
+ task = client.generations.create(
92
+ model: "google/nano-banana",
93
+ mode: "image_to_image",
94
+ prompt: "Turn this reference into a polished ecommerce hero image.",
95
+ image_urls: [uploaded.dig("data", "url")]
96
+ )
97
+ ```
98
+
99
+ Uploads support local paths, IO objects, and `Buble::FileUpload`. Path uploads are streamed from disk.
100
+
101
+ ## Video Generation
102
+
103
+ ```ruby
104
+ task = client.generations.create(
105
+ model: "gork/grok-imagine-video",
106
+ mode: "text_to_video",
107
+ prompt: "A slow cinematic shot of a futuristic train station at sunrise.",
108
+ duration: "5s",
109
+ resolution: "480p",
110
+ aspect_ratio: "16:9"
111
+ )
112
+
113
+ result = client.generations.wait(
114
+ task.dig("data", "id"),
115
+ interval: 2,
116
+ timeout: 900
117
+ )
118
+
119
+ puts result.dig("data", "result", "videos", 0, "url")
120
+ ```
121
+
122
+ Generation request bodies use Buble's flat public API shape. Put model-specific controls in keyword arguments; the SDK serializes those controls at the JSON request root.
123
+
124
+ Do not send internal Buble fields such as `input`, `options`, `scene`, `sub_mode_id`, `provider`, `mediaType`, or `media_type`.
125
+
126
+ ## Apps
127
+
128
+ ```ruby
129
+ app = client.apps.retrieve("video-background-remover")
130
+ puts app.dig("data", "input_parameters")
131
+
132
+ task = client.apps.generations.create("video-background-remover", {
133
+ "source_video" => ["https://example.com/source.mp4"],
134
+ "refine_foreground_edges" => true,
135
+ "subject_is_person" => true
136
+ })
137
+
138
+ result = client.apps.generations.wait("video-background-remover", task.dig("data", "id"))
139
+ ```
140
+
141
+ Apps are preconfigured workflows. Only send parameter names returned by `client.apps.list` or `client.apps.retrieve(...)`.
142
+
143
+ ## Chat
144
+
145
+ ### OpenAI-Compatible
146
+
147
+ ```ruby
148
+ completion = client.chat.completions.create(
149
+ model: "openai/gpt-5.4",
150
+ messages: [
151
+ { role: "user", content: "Write a short launch summary." }
152
+ ],
153
+ max_completion_tokens: 800
154
+ )
155
+
156
+ puts completion.dig("choices", 0, "message", "content")
157
+ ```
158
+
159
+ ### Streaming
160
+
161
+ ```ruby
162
+ client.chat.completions.stream_text(
163
+ model: "openai/gpt-5.4",
164
+ messages: [
165
+ { role: "user", content: "Write one sentence at a time." }
166
+ ]
167
+ ).each do |text|
168
+ print text
169
+ end
170
+ ```
171
+
172
+ ### Anthropic-Compatible
173
+
174
+ ```ruby
175
+ message = client.chat.messages.create(
176
+ model: "openai/gpt-5.4",
177
+ system: "You are concise.",
178
+ messages: [
179
+ { role: "user", content: "Summarize this release." }
180
+ ],
181
+ max_tokens: 800
182
+ )
183
+ ```
184
+
185
+ ### Gemini-Compatible
186
+
187
+ ```ruby
188
+ response = client.chat.gemini.generate_content("openai/gpt-5.4", {
189
+ contents: [
190
+ {
191
+ role: "user",
192
+ parts: [
193
+ { text: "Write a short launch summary." }
194
+ ]
195
+ }
196
+ ]
197
+ })
198
+ ```
199
+
200
+ Gemini streaming uses `stream_generate_content`, not `stream: true` on `generate_content`.
201
+
202
+ Chat methods preserve protocol-native response shapes as Ruby Hashes with string keys.
203
+
204
+ ## Error Handling
205
+
206
+ ```ruby
207
+ begin
208
+ client.generations.retrieve("task_id")
209
+ rescue Buble::APIError => error
210
+ warn error.status
211
+ warn error.code
212
+ warn error.message
213
+ warn error.details
214
+ end
215
+
216
+ begin
217
+ client.generations.wait("task_id")
218
+ rescue Buble::GenerationFailedError => error
219
+ warn error.task["error"]
220
+ end
221
+ ```
222
+
223
+ ## Development
224
+
225
+ ```bash
226
+ cd ruby
227
+ bundle install
228
+ bundle exec rake test
229
+ bundle exec rubocop
230
+ gem build buble.gemspec
231
+ ```
232
+
233
+ Live smoke test:
234
+
235
+ ```bash
236
+ BUBLE_API_KEY=sk_... ruby -Ilib tools/live_smoke.rb
237
+ ```
238
+
239
+ The live smoke test calls discovery and chat endpoints only and does not create billable generation tasks.
240
+
241
+ ## Publishing Checklist
242
+
243
+ RubyGems package identity:
244
+
245
+ - Gem name: `buble`
246
+ - Namespace: `Buble`
247
+ - License: MIT
248
+ - Homepage: `https://buble.ai/`
249
+
250
+ Build and publish:
251
+
252
+ ```bash
253
+ cd ruby
254
+ bundle exec rake test
255
+ bundle exec rubocop
256
+ gem build buble.gemspec
257
+ ```
258
+
259
+ Publication is handled by the monorepo `Release Ruby SDK` GitHub Actions workflow. Configure RubyGems Trusted Publishing for:
260
+
261
+ - Repository owner: `bublehq`
262
+ - Repository name: `sdks`
263
+ - Workflow filename: `release-ruby-sdk.yml`
264
+ - Environment: `release`
265
+
266
+ Then publish from the monorepo with:
267
+
268
+ ```bash
269
+ git tag ruby-v0.1.0
270
+ git push origin ruby-v0.1.0
271
+ ```
272
+
273
+ RubyGems versions are immutable. After `0.1.0` is published, fixes must use a new version such as `0.1.1`.
@@ -0,0 +1,145 @@
1
+ # Buble Ruby SDK Technical Design
2
+
3
+ ## Goals
4
+
5
+ The Ruby SDK mirrors the public Buble API in the same style as the existing JavaScript, Python, Go, Java, .NET, and PHP SDKs.
6
+
7
+ The SDK should:
8
+
9
+ - Keep the public API shape close to Buble's HTTP API.
10
+ - Preserve protocol-native chat response shapes.
11
+ - Use flat generation request bodies.
12
+ - Reject internal Buble workflow fields before sending requests.
13
+ - Avoid runtime dependencies outside the Ruby standard library.
14
+ - Be safe for server-side use and never encourage client-side API key exposure.
15
+
16
+ ## Package Shape
17
+
18
+ The gem is named `buble` and exposes the `Buble` namespace.
19
+
20
+ Primary entry point:
21
+
22
+ ```ruby
23
+ require "buble"
24
+
25
+ client = Buble::Client.new
26
+ ```
27
+
28
+ The package root contains the gemspec and build metadata. Runtime source lives under `lib/`.
29
+
30
+ ## HTTP Layer
31
+
32
+ `Buble::HTTP` is a small wrapper around `Net::HTTP`.
33
+
34
+ Responsibilities:
35
+
36
+ - Resolve base URL and paths.
37
+ - Add bearer token authentication.
38
+ - Encode query strings.
39
+ - Encode JSON request bodies.
40
+ - Decode JSON responses.
41
+ - Raise `Buble::APIError` for non-2xx responses.
42
+ - Send multipart file uploads.
43
+ - Stream SSE response lines.
44
+
45
+ The SDK intentionally avoids Faraday or other HTTP dependencies. This keeps installation lightweight and reduces dependency surface for Rails and non-Rails server applications.
46
+
47
+ ## Resources
48
+
49
+ Resources map directly to API areas:
50
+
51
+ - `Buble::MediaModelsResource`
52
+ - `Buble::FilesResource`
53
+ - `Buble::GenerationsResource`
54
+ - `Buble::AppsResource`
55
+ - `Buble::AppGenerationsResource`
56
+ - `Buble::ChatResource`
57
+ - `Buble::ChatModelsResource`
58
+ - `Buble::ChatCompletionsResource`
59
+ - `Buble::MessagesResource`
60
+ - `Buble::GeminiResource`
61
+
62
+ Responses are Ruby Hashes with string keys. This preserves Buble API response shapes and avoids symbolizing arbitrary server-provided keys.
63
+
64
+ ## Generation Requests
65
+
66
+ Generation requests use keyword arguments for stable fields and `**params` for model-specific controls:
67
+
68
+ ```ruby
69
+ client.generations.create(
70
+ model: "google/nano-banana",
71
+ mode: "text_to_image",
72
+ prompt: "A product photo",
73
+ aspect_ratio: "1:1",
74
+ output_format: "png"
75
+ )
76
+ ```
77
+
78
+ The resulting HTTP body is flat:
79
+
80
+ ```json
81
+ {
82
+ "model": "google/nano-banana",
83
+ "mode": "text_to_image",
84
+ "prompt": "A product photo",
85
+ "aspect_ratio": "1:1",
86
+ "output_format": "png"
87
+ }
88
+ ```
89
+
90
+ The SDK rejects internal fields:
91
+
92
+ - `input`
93
+ - `options`
94
+ - `scene`
95
+ - `sub_mode_id`
96
+ - `subModeId`
97
+ - `provider`
98
+ - `mediaType`
99
+ - `media_type`
100
+ - `images`
101
+ - `image_input`
102
+ - `video_input`
103
+ - `audio_input`
104
+
105
+ ## Streaming
106
+
107
+ `Buble::Streaming::SSEParser` parses server-sent event lines into `Buble::Streaming::Event` objects.
108
+
109
+ Text helpers extract deltas for:
110
+
111
+ - OpenAI-compatible chat: `choices[0].delta.content`
112
+ - Anthropic-compatible messages: `delta.text`
113
+ - Gemini-compatible chat: `candidates[0].content.parts[0].text`
114
+
115
+ The lower-level `stream` APIs expose parsed SSE events for callers that need protocol details.
116
+
117
+ ## Errors
118
+
119
+ The error hierarchy is:
120
+
121
+ - `Buble::Error`
122
+ - `Buble::APIError`
123
+ - `Buble::TimeoutError`
124
+ - `Buble::GenerationFailedError`
125
+ - `Buble::GenerationCanceledError`
126
+ - `Buble::UnsupportedGenerationFieldError`
127
+
128
+ `APIError` carries HTTP status, API error code, details, and raw response body.
129
+
130
+ ## Testing
131
+
132
+ Unit tests use Minitest and `FakeTransport`.
133
+
134
+ Coverage focuses on:
135
+
136
+ - Request paths and bodies.
137
+ - Flat generation request serialization.
138
+ - Forbidden generation field rejection.
139
+ - Wait polling behavior.
140
+ - Multipart upload shape.
141
+ - App generation paths.
142
+ - Chat response preservation.
143
+ - SSE parsing and text extraction.
144
+
145
+ Live smoke tests are intentionally limited to low-cost discovery and chat checks.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ message = client.chat.messages.create(
8
+ model: 'openai/gpt-5.4',
9
+ system: 'You are concise.',
10
+ messages: [
11
+ { role: 'user', content: 'Summarize this release.' }
12
+ ],
13
+ max_tokens: 800
14
+ )
15
+
16
+ puts message
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ task = client.apps.generations.create('video-background-remover', {
8
+ 'source_video' => ['https://example.com/source.mp4'],
9
+ 'refine_foreground_edges' => true,
10
+ 'subject_is_person' => true
11
+ })
12
+
13
+ result = client.apps.generations.wait('video-background-remover', task.dig('data', 'id'))
14
+ puts result.dig('data', 'result', 'videos', 0, 'url')
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ response = client.chat.gemini.generate_content('openai/gpt-5.4', {
8
+ contents: [
9
+ {
10
+ role: 'user',
11
+ parts: [
12
+ { text: 'Write a short launch summary.' }
13
+ ]
14
+ }
15
+ ]
16
+ })
17
+
18
+ puts response
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ uploaded = client.files.upload(
8
+ Buble::FileUpload.from_path('reference.png', content_type: 'image/png'),
9
+ file_type: 'image',
10
+ model: 'google/nano-banana',
11
+ mode: 'image_to_image'
12
+ )
13
+
14
+ task = client.generations.create(
15
+ model: 'google/nano-banana',
16
+ mode: 'image_to_image',
17
+ prompt: 'Turn this reference into a polished ecommerce hero image.',
18
+ image_urls: [uploaded.dig('data', 'url')]
19
+ )
20
+
21
+ result = client.generations.wait(task.dig('data', 'id'))
22
+ puts result.dig('data', 'result', 'images', 0, 'url')
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ completion = client.chat.completions.create(
8
+ model: 'openai/gpt-5.4',
9
+ messages: [
10
+ { role: 'user', content: 'Write a short launch summary.' }
11
+ ],
12
+ max_completion_tokens: 800
13
+ )
14
+
15
+ puts completion.dig('choices', 0, 'message', 'content')
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ task = client.generations.create(
8
+ model: 'google/nano-banana',
9
+ mode: 'text_to_image',
10
+ prompt: 'A cinematic product photo of a matte black espresso cup',
11
+ aspect_ratio: '1:1',
12
+ output_format: 'png'
13
+ )
14
+
15
+ result = client.generations.wait(task.dig('data', 'id'))
16
+ puts result.dig('data', 'result', 'images', 0, 'url')
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'buble'
4
+
5
+ client = Buble::Client.new
6
+
7
+ task = client.generations.create(
8
+ model: 'gork/grok-imagine-video',
9
+ mode: 'text_to_video',
10
+ prompt: 'A slow cinematic shot of a futuristic train station at sunrise.',
11
+ duration: '5s',
12
+ resolution: '480p',
13
+ aspect_ratio: '16:9'
14
+ )
15
+
16
+ result = client.generations.wait(task.dig('data', 'id'), interval: 2, timeout: 900)
17
+ puts result.dig('data', 'result', 'videos', 0, 'url')
data/lib/buble/apps.rb ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buble
4
+ class AppGenerationsResource
5
+ TERMINAL_STATUSES = GenerationsResource::TERMINAL_STATUSES
6
+
7
+ def initialize(http)
8
+ @http = http
9
+ end
10
+
11
+ def create(app_id, params = {})
12
+ @http.request('POST', "/api/v1/apps/#{HTTP.encode_segment(app_id)}/generations", body: params)
13
+ end
14
+
15
+ def retrieve(app_id, id)
16
+ @http.request('GET', "/api/v1/apps/#{HTTP.encode_segment(app_id)}/generations/#{HTTP.encode_segment(id)}")
17
+ end
18
+
19
+ def wait(app_id, id, interval: 2, timeout: 600, throw_on_failed: true, throw_on_canceled: true)
20
+ deadline = Time.now + timeout
21
+
22
+ loop do
23
+ envelope = retrieve(app_id, id)
24
+ task = envelope['data'] || {}
25
+ status = task['status']
26
+ if TERMINAL_STATUSES.include?(status)
27
+ raise_if_terminal_error!(id, task, status, throw_on_failed, throw_on_canceled)
28
+ return envelope
29
+ end
30
+
31
+ if Time.now >= deadline
32
+ raise TimeoutError.new(
33
+ "App generation #{id} did not finish within #{timeout} seconds.",
34
+ timeout: timeout
35
+ )
36
+ end
37
+
38
+ sleep interval
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def raise_if_terminal_error!(id, task, status, throw_on_failed, throw_on_canceled)
45
+ if status == 'failed' && throw_on_failed
46
+ error = task['error'] || {}
47
+ raise GenerationFailedError.new(error['message'] || 'Generation failed.', task: task)
48
+ end
49
+
50
+ return unless status == 'canceled' && throw_on_canceled
51
+
52
+ raise GenerationCanceledError.new("App generation #{id} was canceled.", task: task)
53
+ end
54
+ end
55
+
56
+ class AppsResource
57
+ attr_reader :generations
58
+
59
+ def initialize(http)
60
+ @http = http
61
+ @generations = AppGenerationsResource.new(http)
62
+ end
63
+
64
+ def list
65
+ @http.request('GET', '/api/v1/apps')
66
+ end
67
+
68
+ def retrieve(app_id)
69
+ @http.request('GET', "/api/v1/apps/#{HTTP.encode_segment(app_id)}")
70
+ end
71
+ end
72
+ end