moondream-client 0.0.1

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: 307f9a7735718231a95a7a68764c0958bd7e1f1183ceefd24bf1b89b82050e64
4
+ data.tar.gz: 21646d85a3be9d0eed1682e0038fb005dc3949de31538804e6a14dcbecf7010e
5
+ SHA512:
6
+ metadata.gz: d2703bc5d2f4db1db9fa20686c8b43ac0acc16b70f137ecea334f4d9a289d0703ac54a27231d2f71e6cebdb03420c53e345c26c4d9bc0f2d19dcc6b7d6851f9c
7
+ data.tar.gz: 03becd543a92f77ad0b75b1e848df1c09dff3c66d3bc966e988cce05e767d44a91b9fec90914bd35e0545b88878da957e1e95a5797f5e09585e0d793362a7704
data/.env.example ADDED
@@ -0,0 +1 @@
1
+ MOONDREAM_ACCESS_TOKEN=
data/.rubocop.yml ADDED
@@ -0,0 +1,42 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.3.9
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Style/Documentation:
7
+ Enabled: false
8
+
9
+ Bundler/OrderedGems:
10
+ Enabled: false
11
+
12
+ Style/StringLiterals:
13
+ EnforcedStyle: double_quotes
14
+
15
+ Style/FrozenStringLiteralComment:
16
+ SafeAutoCorrect: true
17
+ EnforcedStyle: always_true
18
+
19
+ Metrics/MethodLength:
20
+ Enabled: false
21
+
22
+ Style/AccessorGrouping:
23
+ Enabled: false
24
+
25
+ Metrics/AbcSize:
26
+ Enabled: false
27
+
28
+ Metrics/CyclomaticComplexity:
29
+ Enabled: false
30
+
31
+ Metrics/ParameterLists:
32
+ Enabled: false
33
+
34
+ Metrics/ClassLength:
35
+ Enabled: false
36
+
37
+ Style/HashSyntax:
38
+ Enabled: false
39
+
40
+ Naming/FileName:
41
+ Exclude:
42
+ - "lib/moondream-client.rb"
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "minitest", "~> 5.0"
8
+ gem "rake", "~> 13.0"
9
+ gem "rubocop", "~> 1.21"
10
+ gem "dotenv"
data/Gemfile.lock ADDED
@@ -0,0 +1,68 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ moondream-client (0.0.1)
5
+ base64
6
+ faraday (>= 1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.3)
12
+ base64 (0.2.0)
13
+ dotenv (3.1.8)
14
+ faraday (2.14.0)
15
+ faraday-net_http (>= 2.0, < 3.5)
16
+ json
17
+ logger
18
+ faraday-net_http (3.4.1)
19
+ net-http (>= 0.5.0)
20
+ json (2.15.1)
21
+ language_server-protocol (3.17.0.5)
22
+ lint_roller (1.1.0)
23
+ logger (1.7.0)
24
+ minitest (5.25.5)
25
+ net-http (0.6.0)
26
+ uri
27
+ parallel (1.27.0)
28
+ parser (3.3.9.0)
29
+ ast (~> 2.4.1)
30
+ racc
31
+ prism (1.5.1)
32
+ racc (1.8.1)
33
+ rainbow (3.1.1)
34
+ rake (13.3.0)
35
+ regexp_parser (2.11.3)
36
+ rubocop (1.81.1)
37
+ json (~> 2.3)
38
+ language_server-protocol (~> 3.17.0.2)
39
+ lint_roller (~> 1.1.0)
40
+ parallel (~> 1.10)
41
+ parser (>= 3.3.0.2)
42
+ rainbow (>= 2.2.2, < 4.0)
43
+ regexp_parser (>= 2.9.3, < 3.0)
44
+ rubocop-ast (>= 1.47.1, < 2.0)
45
+ ruby-progressbar (~> 1.7)
46
+ unicode-display_width (>= 2.4.0, < 4.0)
47
+ rubocop-ast (1.47.1)
48
+ parser (>= 3.3.7.2)
49
+ prism (~> 1.4)
50
+ ruby-progressbar (1.13.0)
51
+ unicode-display_width (3.2.0)
52
+ unicode-emoji (~> 4.1)
53
+ unicode-emoji (4.1.0)
54
+ uri (1.0.4)
55
+
56
+ PLATFORMS
57
+ arm64-darwin-24
58
+ ruby
59
+
60
+ DEPENDENCIES
61
+ dotenv
62
+ minitest (~> 5.0)
63
+ moondream-client!
64
+ rake (~> 13.0)
65
+ rubocop (~> 1.21)
66
+
67
+ BUNDLED WITH
68
+ 2.5.7
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 851 Inc.
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,132 @@
1
+ # MoondreamClient
2
+
3
+ Ruby client for the Moondream API, providing typed classes for Caption, Query, Detect, and Point, plus streaming captions.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application"s Gemfile by executing:
8
+
9
+ $ bundle add moondream-client
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install moondream-client
14
+
15
+ ## Usage
16
+
17
+ ### Configuration
18
+
19
+ Configure the client once at boot (e.g., in Rails an initializer) using `MoondreamClient.configure`.
20
+
21
+ Get an access token from the Moondream Cloud: https://moondream.ai/c/cloud/api-keys.
22
+
23
+ ```ruby
24
+ MoondreamClient.configure do |config|
25
+ config.access_token = ENV["MOONDREAM_ACCESS_TOKEN"]
26
+ config.uri_base = "https://api.moondream.ai/v1" # Optional (default: https://api.moondream.ai/v1)
27
+ config.request_timeout = 120 # Optional (default: 120)
28
+ end
29
+ ```
30
+
31
+ ### Caption
32
+
33
+ Create captions via the `/caption` endpoint. If you pass an `http(s)` image URL, the client will download it and convert it to a base64 data URL automatically.
34
+
35
+ ```ruby
36
+ caption = MoondreamClient::Caption.create!(
37
+ image_url: "data:image/jpeg;base64,..." # or https URL,
38
+ length: "short" # or "normal",
39
+ stream: false
40
+ )
41
+ caption.caption # => String caption text
42
+ caption.request_id # => String request id
43
+ ```
44
+
45
+ #### Streaming captions
46
+
47
+ You can stream caption chunks and get a final `Caption` object at the end:
48
+
49
+ ```ruby
50
+ final = MoondreamClient::Caption.stream!(
51
+ image_url: "data:image/jpeg;base64,...", # or https URL
52
+ length: "short"
53
+ ) do |chunk|
54
+ print chunk # chunk is a String
55
+ end
56
+
57
+ final.caption # => String final caption
58
+ ```
59
+
60
+ ### Query
61
+
62
+ Ask questions about an image via `/query`. `http(s)` URLs are automatically converted to base64 data URLs.
63
+
64
+ ```ruby
65
+ query = MoondreamClient::Query.create!(
66
+ image_url: "data:image/jpeg;base64,...",
67
+ question: "What color is the car?"
68
+ )
69
+ query.answer # => String answer text
70
+ query.request_id # => String request id
71
+ ```
72
+
73
+ ### Detect
74
+
75
+ Detect objects and return bounding boxes via `/detect`. `http(s)` URLs are automatically converted to base64 data URLs.
76
+
77
+ ```ruby
78
+ detect = MoondreamClient::Detect.create!(
79
+ image_url: "data:image/jpeg;base64,...",
80
+ object: "person"
81
+ )
82
+ detect.objects # => [#<BoundingBox x_min y_min x_max y_max>]
83
+ detect.request_id # => String request id
84
+ ```
85
+
86
+ ### Point
87
+
88
+ Locate center points for objects via `/point`. `http(s)` URLs are automatically converted to base64 data URLs.
89
+
90
+ ```ruby
91
+ point = MoondreamClient::Point.create!(
92
+ image_url: "data:image/jpeg;base64,...",
93
+ object: "face"
94
+ )
95
+ point.points # => [#<Coordinate x y>]
96
+ point.request_id # => String request id
97
+ ```
98
+
99
+ ### Error handling
100
+
101
+ HTTP and API errors raise typed exceptions:
102
+
103
+ - `MoondreamClient::UnauthorizedError` (401)
104
+ - `MoondreamClient::ForbiddenError` (403)
105
+ - `MoondreamClient::NotFoundError` (404)
106
+ - `MoondreamClient::ServerError` (other non-success)
107
+
108
+ Rescue them as needed:
109
+
110
+ ```ruby
111
+ begin
112
+ MoondreamClient::Caption.create!(
113
+ image_url: "data:image/jpeg;base64,...",
114
+ length: "short"
115
+ )
116
+ rescue MoondreamClient::UnauthorizedError
117
+ # handle invalid/missing token
118
+ end
119
+ ```
120
+
121
+ ## Development
122
+
123
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124
+
125
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to rubygems.org.
126
+
127
+ For local development, copy the example environment file and set your API token so `bin/console` can load it automatically:
128
+
129
+ ```
130
+ cp .env.example .env
131
+ echo 'MOONDREAM_ACCESS_TOKEN=your_api_token_here' >> .env
132
+ ```
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoondreamClient
4
+ class Caption
5
+ # @return [String] The server-generated request identifier.
6
+ attr_reader :request_id
7
+
8
+ # @return [String] The generated caption text.
9
+ attr_reader :caption
10
+
11
+ # Initialize a new Caption result object.
12
+ #
13
+ # @param attributes [Hash] Raw attributes from the /caption endpoint response.
14
+ # @param client [MoondreamClient::Client]
15
+ def initialize(attributes, client: MoondreamClient.client)
16
+ @client = client
17
+ reset_attributes(attributes)
18
+ end
19
+
20
+ class << self
21
+ # Create a caption for an image.
22
+ # Corresponds to POST /caption
23
+ #
24
+ # @param image_url [String] A URL or data URL for the image.
25
+ # @param length [String] "short" or "normal" (default).
26
+ # @param stream [Boolean] Whether to stream the response (default: false).
27
+ # @param client [MoondreamClient::Client]
28
+ #
29
+ # @return [MoondreamClient::Caption]
30
+ def create!(image_url:, length: "normal", stream: false, client: MoondreamClient.client)
31
+ image_data_url = MoondreamClient::Image.to_data_url(image_url)
32
+ payload = {
33
+ image_url: image_data_url,
34
+ length: length,
35
+ stream: stream
36
+ }
37
+
38
+ attributes = client.post("/caption", payload)
39
+ new(attributes, client: client)
40
+ end
41
+
42
+ # Stream caption chunks and return the final Caption instance.
43
+ #
44
+ # @param image_url [String]
45
+ # @param length [String]
46
+ # @param client [MoondreamClient::Client]
47
+ # @yield [chunk] yields each text chunk String as it arrives
48
+ # @return [MoondreamClient::Caption]
49
+ def stream!(image_url:, length: "normal", client: MoondreamClient.client, &block)
50
+ image_data_url = MoondreamClient::Image.to_data_url(image_url)
51
+ payload = {
52
+ image_url: image_data_url,
53
+ length: length,
54
+ stream: true
55
+ }
56
+
57
+ caption = nil
58
+
59
+ client.post_stream("/caption", payload) do |data|
60
+ if (chunk = data["chunk"]) && !chunk.to_s.empty?
61
+ block&.call(chunk)
62
+ end
63
+
64
+ caption = data["caption"] if data["caption"]
65
+ end
66
+
67
+ # Build the final Caption from aggregated stream content.
68
+ new({ "caption" => caption }, client: client)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ # Normalize attributes from the /caption response.
75
+ # @param attributes [Hash]
76
+ # @return [void]
77
+ def reset_attributes(attributes)
78
+ @request_id = attributes["request_id"]
79
+ @caption = attributes["caption"]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoondreamClient
4
+ class Client
5
+ # The configuration for the client.
6
+ #
7
+ # @return [MoondreamClient::Configuration]
8
+ attr_accessor :configuration
9
+
10
+ # Initialize the client.
11
+ #
12
+ # @param configuration [MoondreamClient::Configuration] The configuration for the client.
13
+ #
14
+ # @return [MoondreamClient::Client]
15
+ def initialize(configuration = MoondreamClient.configuration)
16
+ @configuration = configuration
17
+ end
18
+
19
+ # Make a POST request to the API.
20
+ #
21
+ # @param path [String] The path to the API endpoint.
22
+ # @param payload [Hash] The payload to send to the API.
23
+ # @param headers [Hash] The headers to send to the API.
24
+ #
25
+ # @return [Hash] The response from the API.
26
+ def post(path, payload, headers: {})
27
+ response = connection.post(build_url(path)) do |request|
28
+ request.headers["X-Moondream-Auth"] = @configuration.access_token if @configuration.access_token
29
+ request.headers["Content-Type"] = "application/json"
30
+ request.headers["Accept"] = "application/json"
31
+ request.headers.merge!(headers)
32
+ request.body = payload.compact.to_json
33
+ end
34
+
35
+ handle_error(response) unless response.success?
36
+
37
+ JSON.parse(response.body)
38
+ end
39
+
40
+ # Make a GET request to the API.
41
+ #
42
+ # @param path [String] The path to the API endpoint.
43
+ #
44
+ # @return [Hash] The response from the API.
45
+ def get(path)
46
+ response = connection.get(build_url(path)) do |request|
47
+ request.headers["X-Moondream-Auth"] = @configuration.access_token if @configuration.access_token
48
+ request.headers["Content-Type"] = "application/json"
49
+ end
50
+
51
+ handle_error(response) unless response.success?
52
+
53
+ JSON.parse(response.body)
54
+ end
55
+
56
+ # Make a streaming POST request to the API using text/event-stream.
57
+ # Parses SSE lines and yields decoded event data Hashes to the provided block.
58
+ #
59
+ # @param path [String]
60
+ # @param payload [Hash]
61
+ # @param headers [Hash]
62
+ # @yield [data] yields parsed JSON from each SSE event's data field
63
+ # @return [void]
64
+ def post_stream(path, payload = {}, headers: {}, &block)
65
+ decoder = SSEDecoder.new
66
+ buffer = ""
67
+
68
+ connection.post(build_url(path)) do |request|
69
+ request.headers["X-Moondream-Auth"] = @configuration.access_token if @configuration.access_token
70
+ request.headers["Accept"] = "text/event-stream"
71
+ request.headers["Cache-Control"] = "no-store"
72
+ request.headers["Content-Type"] = "application/json"
73
+ request.headers.merge!(headers)
74
+ request.body = payload.compact.to_json
75
+ request.options.on_data = lambda { |chunk, _total_bytes, _env|
76
+ # Normalize and split into lines, preserving last partial line in buffer
77
+ buffer = (buffer + chunk.to_s).gsub(/\r\n?/, "\n")
78
+ lines = buffer.split("\n", -1)
79
+ buffer = lines.pop || ""
80
+ lines.each do |line|
81
+ event = decoder.decode(line)
82
+ block&.call(event["data"]) if event && event["data"]
83
+ end
84
+ }
85
+ end
86
+ end
87
+
88
+ # Handle errors from the API.
89
+ #
90
+ # @param response [Faraday::Response] The response from the API.
91
+ #
92
+ # @return [void]
93
+ def handle_error(response)
94
+ case response.status
95
+ when 401
96
+ raise UnauthorizedError, response.body
97
+ when 403
98
+ raise ForbiddenError, response.body
99
+ when 404
100
+ raise NotFoundError, response.body
101
+ else
102
+ raise ServerError, response.body
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # Minimal SSE decoder for parsing standard server-sent event lines.
109
+ class SSEDecoder
110
+ def initialize
111
+ @event = ""
112
+ @data = ""
113
+ @id = nil
114
+ @retry = nil
115
+ end
116
+
117
+ # @param line [String]
118
+ # @return [Hash, nil]
119
+ def decode(line)
120
+ return flush_event if line.empty?
121
+ return if line.start_with?(":")
122
+
123
+ field, _, value = line.partition(":")
124
+ value = value.lstrip
125
+
126
+ case field
127
+ when "event"
128
+ @event = value
129
+ when "data"
130
+ @data += "#{value}\n"
131
+ when "id"
132
+ @id = value
133
+ when "retry"
134
+ @retry = value.to_i
135
+ end
136
+
137
+ nil
138
+ end
139
+
140
+ private
141
+
142
+ def flush_event
143
+ return if @data.empty?
144
+
145
+ data = @data.chomp
146
+ parsed = JSON.parse(data)
147
+
148
+ event = { "data" => parsed }
149
+ event["event"] = @event unless @event.empty?
150
+ event["id"] = @id if @id
151
+ event["retry"] = @retry if @retry
152
+
153
+ @event = ""
154
+ @data = ""
155
+ @id = nil
156
+ @retry = nil
157
+
158
+ event
159
+ end
160
+ end
161
+
162
+ # Build the URL for the API.
163
+ #
164
+ # @param path [String] The path to the API endpoint.
165
+ def build_url(path)
166
+ "#{@configuration.uri_base}#{path}"
167
+ end
168
+
169
+ # Create a connection to the API.
170
+ #
171
+ # @return [Faraday::Connection]
172
+ def connection
173
+ Faraday.new do |faraday|
174
+ faraday.request :url_encoded
175
+ faraday.options.timeout = @configuration.request_timeout
176
+ faraday.options.open_timeout = @configuration.request_timeout
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoondreamClient
4
+ class Detect
5
+ class BoundingBox
6
+ # @return [Float] Normalized minimum x coordinate (0.0..1.0)
7
+ attr_reader :x_min
8
+ # @return [Float] Normalized minimum y coordinate (0.0..1.0)
9
+ attr_reader :y_min
10
+ # @return [Float] Normalized maximum x coordinate (0.0..1.0)
11
+ attr_reader :x_max
12
+ # @return [Float] Normalized maximum y coordinate (0.0..1.0)
13
+ attr_reader :y_max
14
+
15
+ def initialize(x_min:, y_min:, x_max:, y_max:)
16
+ @x_min = x_min
17
+ @y_min = y_min
18
+ @x_max = x_max
19
+ @y_max = y_max
20
+ end
21
+ end
22
+
23
+ # @return [String] The server-generated request identifier.
24
+ attr_reader :request_id
25
+
26
+ # @return [Array<BoundingBox>] The list of detected object bounding boxes.
27
+ attr_reader :objects
28
+
29
+ # Initialize a new Detect result object.
30
+ #
31
+ # @param attributes [Hash] Raw attributes from the /detect endpoint response.
32
+ # @param client [MoondreamClient::Client]
33
+ def initialize(attributes, client: MoondreamClient.client)
34
+ @client = client
35
+ reset_attributes(attributes)
36
+ end
37
+
38
+ class << self
39
+ # Detect objects described by `object` within an image.
40
+ # Corresponds to POST /detect
41
+ #
42
+ # @param image_url [String]
43
+ # @param object [String] Object description, e.g. "person".
44
+ # @param client [MoondreamClient::Client]
45
+ #
46
+ # @return [MoondreamClient::Detect]
47
+ def create!(image_url:, object:, client: MoondreamClient.client)
48
+ image_data_url = MoondreamClient::Image.to_data_url(image_url)
49
+ payload = {
50
+ image_url: image_data_url,
51
+ object: object
52
+ }
53
+
54
+ attributes = client.post("/detect", payload)
55
+ new(attributes, client: client)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ # Normalize attributes from the /detect response.
62
+ # @param attributes [Hash]
63
+ # @return [void]
64
+ def reset_attributes(attributes)
65
+ @request_id = attributes["request_id"]
66
+ @objects = Array(attributes["objects"]).map do |o|
67
+ BoundingBox.new(x_min: o["x_min"], y_min: o["y_min"], x_max: o["x_max"], y_max: o["y_max"])
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module MoondreamClient
6
+ module Image
7
+ class << self
8
+ # Convert an input reference into a data URL.
9
+ # If it is already a data URL, return as-is.
10
+ # If it is an HTTP(S) URL, download and convert to base64 data URL.
11
+ # Otherwise, return as-is.
12
+ #
13
+ # @param reference [String]
14
+ # @return [String]
15
+ def to_data_url(reference)
16
+ return reference if data_url?(reference)
17
+
18
+ return http_to_data_url(reference) if http_url?(reference)
19
+
20
+ reference
21
+ end
22
+
23
+ private
24
+
25
+ def data_url?(value)
26
+ value.is_a?(String) && value.start_with?("data:")
27
+ end
28
+
29
+ def http_url?(value)
30
+ value.is_a?(String) && (value.start_with?("http://") || value.start_with?("https://"))
31
+ end
32
+
33
+ def http_to_data_url(url)
34
+ connection = Faraday.new do |faraday|
35
+ faraday.request :url_encoded
36
+ faraday.options.timeout = MoondreamClient.configuration.request_timeout
37
+ faraday.options.open_timeout = MoondreamClient.configuration.request_timeout
38
+ end
39
+
40
+ response = connection.get(url)
41
+ raise MoondreamClient::ServerError, "Failed to download image: #{response.status}" unless response.success?
42
+
43
+ content_type = response.headers["content-type"] || "image/jpeg"
44
+ base64 = Base64.strict_encode64(response.body)
45
+ "data:#{content_type};base64,#{base64}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoondreamClient
4
+ class Point
5
+ class Coordinate
6
+ # @return [Float] Normalized x coordinate (0.0..1.0)
7
+ attr_reader :x
8
+ # @return [Float] Normalized y coordinate (0.0..1.0)
9
+ attr_reader :y
10
+
11
+ def initialize(x:, y:) # rubocop:disable Naming/MethodParameterName
12
+ @x = x
13
+ @y = y
14
+ end
15
+ end
16
+
17
+ # @return [String] The server-generated request identifier.
18
+ attr_reader :request_id
19
+
20
+ # @return [Array<Coordinate>] The list of point coordinates.
21
+ attr_reader :points
22
+
23
+ # Initialize a new Point result object.
24
+ #
25
+ # @param attributes [Hash] Raw attributes from the /point endpoint response.
26
+ # @param client [MoondreamClient::Client]
27
+ def initialize(attributes, client: MoondreamClient.client)
28
+ @client = client
29
+ reset_attributes(attributes)
30
+ end
31
+
32
+ class << self
33
+ # Locate the center points for objects described by `object` in an image.
34
+ # Corresponds to POST /point
35
+ #
36
+ # @param image_url [String]
37
+ # @param object [String] Object description, e.g. "face".
38
+ # @param client [MoondreamClient::Client]
39
+ #
40
+ # @return [MoondreamClient::Point]
41
+ def create!(image_url:, object:, client: MoondreamClient.client)
42
+ image_data_url = MoondreamClient::Image.to_data_url(image_url)
43
+ payload = {
44
+ image_url: image_data_url,
45
+ object: object
46
+ }
47
+
48
+ attributes = client.post("/point", payload)
49
+ new(attributes, client: client)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Normalize attributes from the /point response.
56
+ # @param attributes [Hash]
57
+ # @return [void]
58
+ def reset_attributes(attributes)
59
+ @request_id = attributes["request_id"]
60
+ @points = Array(attributes["points"]).map do |p|
61
+ Coordinate.new(x: p["x"], y: p["y"])
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoondreamClient
4
+ class Query
5
+ # @return [String] The server-generated request identifier.
6
+ attr_reader :request_id
7
+
8
+ # @return [String] The model's answer text.
9
+ attr_reader :answer
10
+
11
+ # Initialize a new Query result object.
12
+ #
13
+ # @param attributes [Hash] Raw attributes from the /query endpoint response.
14
+ # @param client [MoondreamClient::Client]
15
+ def initialize(attributes, client: MoondreamClient.client)
16
+ @client = client
17
+ reset_attributes(attributes)
18
+ end
19
+
20
+ class << self
21
+ # Ask a question about an image.
22
+ # Corresponds to POST /query
23
+ #
24
+ # @param image_url [String]
25
+ # @param question [String]
26
+ # @param client [MoondreamClient::Client]
27
+ #
28
+ # @return [MoondreamClient::Query]
29
+ def create!(image_url:, question:, client: MoondreamClient.client)
30
+ image_data_url = MoondreamClient::Image.to_data_url(image_url)
31
+ payload = {
32
+ image_url: image_data_url,
33
+ question: question
34
+ }
35
+
36
+ attributes = client.post("/query", payload)
37
+ new(attributes, client: client)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Normalize attributes from the /query response.
44
+ # @param attributes [Hash]
45
+ # @return [void]
46
+ def reset_attributes(attributes)
47
+ @request_id = attributes["request_id"]
48
+ @answer = attributes["answer"]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MoondreamClient
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "time"
5
+
6
+ require_relative "moondream-client/version"
7
+ require_relative "moondream-client/client"
8
+ require_relative "moondream-client/image"
9
+ require_relative "moondream-client/caption"
10
+ require_relative "moondream-client/detect"
11
+ require_relative "moondream-client/point"
12
+ require_relative "moondream-client/query"
13
+
14
+ module MoondreamClient
15
+ class Error < StandardError; end
16
+ class UnauthorizedError < Error; end
17
+ class NotFoundError < Error; end
18
+ class ServerError < Error; end
19
+ class ConfigurationError < Error; end
20
+ class ForbiddenError < Error; end
21
+
22
+ class Configuration
23
+ DEFAULT_URI_BASE = "https://api.moondream.ai/v1"
24
+ DEFAULT_REQUEST_TIMEOUT = 120
25
+
26
+ # The access token for the API.
27
+ #
28
+ # @return [String]
29
+ attr_accessor :access_token
30
+
31
+ # The base URI for the API.
32
+ #
33
+ # @return [String]
34
+ attr_accessor :uri_base
35
+
36
+ # The request timeout in seconds.
37
+ #
38
+ # @return [Integer]
39
+ attr_accessor :request_timeout
40
+
41
+ def initialize
42
+ @access_token = ENV.fetch("MOONDREAM_ACCESS_TOKEN", nil)
43
+ @uri_base = DEFAULT_URI_BASE
44
+ @request_timeout = DEFAULT_REQUEST_TIMEOUT
45
+ end
46
+ end
47
+
48
+ class << self
49
+ # The configuration for the client.
50
+ #
51
+ # @return [MoondreamClient::Configuration]
52
+ def configuration
53
+ @configuration ||= Configuration.new
54
+ end
55
+
56
+ # Allows replacing the configuration object.
57
+ attr_writer :configuration
58
+
59
+ # Configure the client.
60
+ #
61
+ # @yield [MoondreamClient::Configuration] The configuration for the client.
62
+ def configure
63
+ yield(configuration)
64
+ end
65
+
66
+ # The client for the API.
67
+ #
68
+ # @return [MoondreamClient::Client]
69
+ def client
70
+ @client ||= Client.new(configuration)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/moondream-client/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "moondream-client"
7
+ spec.version = MoondreamClient::VERSION
8
+ spec.authors = ["Dylan Player"]
9
+ spec.email = ["dylan@851.sh"]
10
+
11
+ spec.summary = "Ruby client for MoonDream API."
12
+ spec.homepage = "https://github.com/851-labs/moondream-client"
13
+ spec.required_ruby_version = ">= 3.3.0"
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.license = "MIT"
19
+
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
23
+ end
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency("faraday", ">= 1")
30
+ # Base64 becomes a non-default gem in Ruby >= 3.4
31
+ spec.add_dependency("base64", ">= 0")
32
+ spec.metadata["rubygems_mfa_required"] = "true"
33
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: moondream-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dylan Player
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: base64
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - dylan@851.sh
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".env.example"
49
+ - ".rubocop.yml"
50
+ - ".ruby-version"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - lib/moondream-client.rb
57
+ - lib/moondream-client/caption.rb
58
+ - lib/moondream-client/client.rb
59
+ - lib/moondream-client/detect.rb
60
+ - lib/moondream-client/image.rb
61
+ - lib/moondream-client/point.rb
62
+ - lib/moondream-client/query.rb
63
+ - lib/moondream-client/version.rb
64
+ - moondream-client.gemspec
65
+ homepage: https://github.com/851-labs/moondream-client
66
+ licenses:
67
+ - MIT
68
+ metadata:
69
+ allowed_push_host: https://rubygems.org
70
+ homepage_uri: https://github.com/851-labs/moondream-client
71
+ source_code_uri: https://github.com/851-labs/moondream-client
72
+ rubygems_mfa_required: 'true'
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 3.3.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.5.3
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Ruby client for MoonDream API.
92
+ test_files: []