mirador 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- Zjc0NzZjMDljNTNiZjlhNTlhMDlhZGZmYjg1NjRjZmEzZDFiZWU2MA==
4
+ MGZmMTU1ODJkZDdiZjM3MmQ1NzA1MjJkNjkxMjUxMzM1MmMyOTQ1Yg==
5
5
  data.tar.gz: !binary |-
6
- NTg0ZDE2NzYwNmVjOTVhNmU2MDYzMzljODhiNWQzYzE2NTA4NjNhNg==
6
+ OWYyNjc4N2EzMWU1Mjc4YWUxMWJmOGU4NjNhMmFkZjRiZWUzYWYwMA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- Nzg1NDIxOWExZWFjZjQ5YTg3M2YxN2IyYmMyYjZjOGI5MTQ2ODdkYWRhOTJk
10
- NzYzMTQ3MjU4ZWVkNjU5NTlhZjg0ZWIwNDhkNjhkOWFjZTkwOTk0YTlhZTE4
11
- MjRmNzc1NmQyZTAzNGMwMTUwN2E4MmU4NmU4ZGQwYjc2YzE4OWM=
9
+ Mjk2MWU1MDM2ZmNiZDUyYTMwMTRlNzIyMTMyZWY5ZDg5ZDdmNGZhM2FhNDlk
10
+ MDk5MmQ4Nzg3MTI1MjE2ODM1MDY4MjU5YzEzNjAyZmRhYTEwYThmMjI2MTVj
11
+ Mjk1N2IxMGIyNzdhMmQxY2IyN2U3ZGQxZGQxOGRlZmI2ZDAyNDU=
12
12
  data.tar.gz: !binary |-
13
- YzBhODExNjc5OWE2N2E2OThjNDBjOTIwNmJiYjE3ZThlNTBjNzMyMGZiZjFi
14
- ZWRkMjhmOWVhYWY1ZGRkNzhjNTViNWNiNDgwMGUzMTgxZDRiNzNiZDE2MWM3
15
- MzY4NDlmMmVjYmRjOTM1NTUxYTRlMzQ4NDM3ZDQ5NjYxMzJmMDM=
13
+ Yzg0NDBiNTgwODg2MjRlMGVmZTUzNzQ1ZDQ1NTE0ZDhlZTE5MzYxMjhjNTQ0
14
+ MDVkNzFhZGIwNjU5NGQyMmY1MTBhN2RiZmM1MjU0MTJkYTYwNTNmY2EyZjE0
15
+ MmZkNGJmMmY0YzhkZTkwZjgzMjY4ZmFmNjdlNzYzMjdmNDBiNmU=
data/README.md CHANGED
@@ -18,42 +18,257 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- There are really two basic methods available on the API. To get started, you need an API key, available from [mirador.im/join](http://mirador.im/join). If you have problems with the API or this client, please contact support@mirador.im.
21
+ To get started, you need an API key, available from [mirador.im/join](http://mirador.im/). If you have problems with the API or this client, please contact support@mirador.im.
22
22
 
23
- ### `Mirador::Client.classify_files(files) -> [Mirador::Result]`
23
+ ## Mirador::Result and Mirador::ResultList
24
24
 
25
- This method takes a list of filenames and returns a list of `Mirador::Result` objects. See example:
25
+ All multiple-request methods (e.g., classify_files), return a [Mirador::ResultList](#resultlist), which is effectively a list of Mirador::Result objects; single-request methods (e.g., classify_url) return a [Mirador::Result](#result) object.
26
+
27
+ ## Classifying Files & Buffers
28
+
29
+ You can classify 4 types of files/file-objects:
30
+
31
+ * file objects (e.g., `x` where `x = File.open('myfile.jpg')`); [classify_files](#classify_files)
32
+ * filenames `myfile.jpg` [classify_files](#classify_files)
33
+ * buffers `buffer = File.read('myfile.jpg')` [classify_buffers](#classify_buffers)
34
+ * base64-encoded buffers (e.g., from a data URI) [classify_encoded_strings](#classify_encoded_strings)
35
+
36
+ The methods for file-based classification are as follows:
37
+
38
+
39
+ ### <a name="classify_files"></a> Mirador::Client#classify_files
26
40
 
27
41
  ```ruby
28
42
  require 'mirador'
43
+ mc = Mirador::Client.new('your_api_key')
44
+
45
+ results = mc.classify_files('test.jpg', 'picture.jpg')
29
46
 
30
- mc = Mirador::Client.new('your_key_here')
31
- mc.classify_files('bathing-suit.jpg', 'nsfw-user-upload.png').each do |result|
32
- puts "name: #{ result.name }, safe: #{ result.safe }, value: #{ result.value }"
47
+ assert results['test.jpg']
48
+ assert_equal results.length, 2
49
+
50
+ results.each do |res|
51
+ puts "#{ res.id }, #{ res.value }"
52
+ end
53
+
54
+ results.each do |id, res|
55
+ puts "#{ id }, #{ res.value }"
33
56
  end
34
57
 
35
58
  ```
36
59
 
37
- ### `Mirdor::Client.classify_urls(urls) -> [Mirador::Result]`
60
+ You can also specify an id to be used:
61
+
62
+ ```ruby
63
+ require 'mirador'
64
+ mc = Mirador::Client.new 'your_api_key'
65
+
66
+ # first method: use ids as keys
67
+ results = mc.classify_files(nsfw: 'nsfw.jpg', sfw: 'sfw.jpg')
38
68
 
39
- This method takes a list of urls and returns `Mirador::Result` objects. Identical to `classify_files`:
69
+ assert results[:nsfw]
70
+ assert results[:sfw]
71
+
72
+ # second method: pass an array of { id:, data: } hashes
73
+ results = mc.classify_files([{ id: :nsfw, data: 'nsfw.jpg'}, { id: :sfw, data: 'sfw.jpg' }])
74
+
75
+ assert results[:nsfw]
76
+ assert results[:sfw]
77
+ ```
78
+
79
+ File can be either a filename or a file object; e.g., the following is also valid:
80
+
81
+ ```ruby
82
+ results = mc.classify_files(nsfw: File.open('nsfw.jpg'))
83
+ ```
84
+
85
+ ### <a name='classify_file'></a> Mirador::Client#classify_file
86
+
87
+ A shortcut for classifying a single file; this will return a `Mirador::Result` instead of a `Mirador::ResultList`:
40
88
 
41
89
  ```ruby
42
90
  require 'mirador'
91
+ mc = Mirador::Client.new 'your_api_key'
43
92
 
44
- mc.classify_urls('http://possibly-nsfw.com/cool.png', 'http://mysite.net/image/bad-picture.jpg').each do |result|
45
- puts "name: #{ result.name }, safe: #{ result.safe }, value: #{ result.value }"
46
- end
93
+ # first method: use ids as keys
94
+ nsfw = mc.classify_file(nsfw: 'nsfw.jpg')
47
95
 
96
+ puts nsfw.value
48
97
  ```
49
98
 
50
- ### `Mirador::Result`
99
+ ### <a name='classify_buffers'></a> Mirador:Client#classify_buffers
100
+
101
+ Classify a buffer, e.g., an already-read file. This simplifies the classification of file uploads, e.g. POST data. The interface is identical to [classify_files](#classify_files), only differing in the actual data passed in:
102
+
103
+ ```ruby
104
+ require 'mirador'
105
+ mc = Mirador::Client.new 'your_api_key'
106
+
107
+ nsfw_buf = File.read('nsfw.jpg')
108
+ sfw_buf = File.read('sfw.jpg')
109
+
110
+ # these are equivalent
111
+ results = mc.classify_buffers(nsfw: nsfw_buf, sfw: sfw_buf)
112
+ results = mc.classify_buffers([{id: :nsfw, data: nsfw_buf}, {id: :sfw, data: sfw_buf}])
113
+
114
+ # since buffers dont have a name, you just get an index as id
115
+ results = mc.classify_buffers(nsfw_buf, sfw_buf)
116
+ ```
117
+
118
+ #### <a name='classify_buffer'></a> Mirador::Client#classify_buffer
119
+
120
+ As with classify_file, there is a shortcut for classifying only one buffer; see [classify_file](#classify_file) for clarifications on usage (it's identical).
121
+
122
+ ### <a name='classify_encoded_strings'></a> Mirador::Client#classify_encoded_strings
123
+
124
+ The Mirador API internally represents images as base64-encoded strings (agnostic of image encoding); this method lets you pass in an alread-encoded string in the event that you're also using base64 encoding elsewhere in your system. Usage is the same as [classify_buffers](#classify_buffers):
125
+
126
+ ```ruby
127
+ require 'mirador'
128
+ require 'base64'
129
+
130
+ mc = Mirador::Client.new 'your_api_key'
131
+
132
+ nsfw_buf = Base64.encode64(File.read('nsfw.jpg'))
133
+ sfw_buf = Base64.encode64(File.read('sfw.jpg'))
134
+
135
+ # these are equivalent
136
+ results = mc.classify_encoded_strings(nsfw: nsfw_buf, sfw: sfw_buf)
137
+ results = mc.classify_encoded_strings([{id: :nsfw, data: nsfw_buf}, {id: :sfw, data: sfw_buf}])
138
+
139
+ # since strings dont have a name, you just get an index as id
140
+ results = mc.classify_encoded_strings(nsfw_buf, sfw_buf)
141
+ ```
142
+
143
+ #### <a name='classify_encoded_string'></a> Mirador::Client#classify_encoded_string
144
+
145
+ Another helper for only working with 1 request/result at a time. See [classify_file](#classify_file) for more info.
146
+
147
+
148
+ ### <a name='classify_data_uris'></a> Mirador::Client#classify_data_uris
149
+
150
+ This simplifies data transfer between client applications and the mirador API. For example, given the following javascript:
151
+
152
+ ```javascript
153
+ document.getElementById('form-field').addEventListener('change', function (e) {
154
+
155
+ var file = this.files[0];
156
+
157
+ var reader = new FileReader();
158
+ reader.onload = function (e) {
159
+ $.post('/proxy/mirador', { id: file.name, data: e.target.result });
160
+ }
161
+
162
+ reader.readAsDataURL(file);
163
+ });
164
+ ```
165
+
166
+ Your could classify that data url with the following code:
167
+
168
+ ```ruby
169
+
170
+ res = mc.classify_data_uris(request['id'] => request['data'])
171
+
172
+ # send the result
173
+ res[request['id']].to_json
174
+
175
+ # or, even easier
176
+ mc.classify_data_uri(request['id'] => request['data']).to_json
177
+
178
+ ```
179
+
180
+ Otherwise, classify_data_uris and classify_data_uri have identical interfaces to the other methods covered so far.
181
+
182
+
183
+ ## Classify URLs
184
+
185
+ You can easily classify a publically-available URL (e.g., a public s3 bucket), with [classify_urls](#classify_urls) and [classify_url](#classify_url). The interfaces for these methods are identical to the file-handling methods covered above.
186
+
187
+
188
+ ### <a name='classify_urls'></a> Mirador::Client#classify_urls
189
+
190
+ The only things to keep in mind with URLs:
191
+
192
+ * must be publically-accessibly
193
+ * must be < Mirador::Client::MAX_ID_LEN if you are using the url as the item's id (see below)
194
+ * download/response time on url will affect response time of result, must be less than 60 seconds.
195
+
196
+
197
+ #### Examples:
198
+
199
+
200
+ Assigning specific ids to urls:
201
+
202
+ ```ruby
203
+ require 'mirador'
204
+
205
+ mc = Mirador::Client.new 'your_api_key'
206
+
207
+ res = mc.classify_urls(nsfw: 'http://static.mirador.im/test/nsfw.jpg', sfw: 'http://static.mirador.im/test/sfw.jpg')
208
+
209
+ assert res[:nsfw]
210
+ assert res[:sfw].safe
211
+
212
+ ```
213
+
214
+ Implicitly using url as its own id:
215
+
216
+ ```ruby
217
+ require 'mirador'
218
+
219
+ nsfw_url = 'http://static.mirador.im/test/nsfw.jpg'
220
+ sfw_url = 'http://static.mirador.im/test/sfw.jpg'
221
+
222
+ mc = Mirador::Client.new 'your_api_key'
223
+ res = mc.classify_urls(nsfw_url, sfw_url)
224
+
225
+ puts res[nsfw_url].value
226
+ puts res[sfw_url].value
227
+ ```
228
+
229
+ Classify a single URL using Mirador::Client#classify_url
230
+
231
+ ```ruby
232
+ require 'mirador'
233
+
234
+ mc = Mirador::Client.new 'your_api_key'
235
+ nsfw = mc.classify_url(nsfw_url)
236
+
237
+ assert (not nsfw.safe)
238
+ puts nsfw.value
239
+ ```
240
+
241
+ ## <a name='result'></a> Mirador::Result
242
+
243
+ The `Mirador::Result` class wraps the output of the API for a specific image/url. It has the following attributes:
244
+
245
+ * `@id` [Mixed]: the id, as specified in the request, or implied (see above)
246
+ * `@safe` [Boolean]: whether the image should be considered flagged/containing adult content
247
+ * `@value` [Float 0.0-1.0]: A float indicating the likelyhood of the image containing adult content (useful for creating custom thresholds)
248
+ * `@error` [String]: will only be non-nil if this is an error
249
+
250
+ The `Mirador::Result` object also has a couple of convenience methods:
251
+
252
+ * `#to_h` - convert to a hash
253
+ * `#to_json` - if json is require'd, serialize to json
254
+ * `#failed?` - returns a boolean indicating whether image is a failure/error
255
+ * `#to_s` - returns a string representation of the result`
256
+ * `#name` **(deprecated)** - this simply maps to `@id`
257
+
258
+ ## <a name='resultlist'></a> Mirador::ResultList [Enumerable]
259
+
260
+ Methods that return multiple results do so by returning a single `Mirador::ResultList`. This object is used in lieu of a Hash or Array as to provide mixed-access. You can treat it as an array, iterating via `each do |x|`, indexing with integers, or by simply calling `#to_a`, or as a hash, indexing with `@id`'s from image-requests.
261
+
262
+ The ResultList has the following methods:
51
263
 
52
- The `Mirador::Result` class has 3 fields:
264
+ * `#[](key)` operator override to index the ResultList. You can index by integers in range of 0 - ResultList#length, or by an `@id` for one of the Result objects within.
265
+ * `#to_a` convert to an array of `Mirador::Result` objects
266
+ * `#length` the number of items in the `ResultList`
267
+ * `#update` equivalent to Hash#update
268
+ * `#to_h` conver to a hash
269
+ * `#to_json` serialize the resultlist as json
270
+ * `#each` `ResultList` includes `Enumerable`, and this implementation of `#each` checks the arity of blocks passed in to allow iteration either as an array or as a Hash.
53
271
 
54
- * `Result.name` - `string`, the filename or url for this request
55
- * `Result.safe` - `bool`, a boolean indicating whether image contains adult content.
56
- * `Result.value` - `float`, a number 0.0 - 1.0 indicating confidence of judgement
57
272
 
58
273
  ## Contributing
59
274
 
data/lib/mirador.rb CHANGED
@@ -3,137 +3,343 @@ require 'base64'
3
3
 
4
4
  module Mirador
5
5
 
6
- API_BASE = "http://api.mirador.im/v1/"
6
+ class ApiError < StandardError
7
+ end
7
8
 
8
- class Result
9
- attr_accessor :name, :safe, :value
9
+ class ResultList
10
+ include Enumerable
11
+
12
+ def initialize(items=[])
13
+ @items = {}
10
14
 
11
- def initialize name, data
12
- @name = name
13
- @safe = data['safe']
14
- @value = data['value']
15
+ items.each do |x|
16
+ @items[x.id] = x
17
+ end
15
18
  end
16
19
 
17
- def to_s
18
- "<Mirador::Result; name: #{ @name }; safe: #{ @safe }; value: #{ @value }/>"
20
+ def <<(item)
21
+ @items[item.id] = item
22
+ end
23
+
24
+ def [](key)
25
+ if key.is_a? Integer and not @items.has_key? key
26
+ @items.values[key]
27
+ else
28
+ @items[key.to_s]
29
+ end
30
+ end
31
+
32
+ def to_a
33
+ @items.values
34
+ end
35
+
36
+ def length
37
+ @items.values.length
19
38
  end
20
39
 
21
- def self.parse_results reqs, results
40
+ def update other
41
+ @items.update(other)
42
+ end
43
+
44
+ def to_h
45
+ @items
46
+ end
22
47
 
23
- if not results
24
- raise ApiError, "no results for: #{ reqs }"
48
+ def to_json
49
+ @items.to_json
50
+ end
51
+
52
+ def each &block
53
+ if block.arity == 1
54
+ @items.values.each do |x|
55
+ block.call(x)
56
+ end
57
+ else
58
+ @items.each do |k, v|
59
+ block.call(k, v)
60
+ end
25
61
  end
62
+ end
63
+
64
+ def self.parse_results res
26
65
 
27
- results.each_with_index.map do |v, i|
28
- Result.new(reqs[i], v['result'])
66
+ output = {}
67
+ res.each do |x|
68
+ r = Result.new(x)
69
+ output[r.id] = r
29
70
  end
71
+
72
+ output
30
73
  end
31
74
 
32
75
  end
33
76
 
34
- class ApiError < StandardError
77
+ class Result
78
+ attr_accessor :id, :safe, :value, :error
79
+
80
+ def initialize data
81
+
82
+ if data.has_key? 'errors'
83
+ @error = data['errors']
84
+ return
85
+ end
86
+
87
+ @id = data['id']
88
+ @safe = data['result']['safe']
89
+ @value = data['result']['value']
90
+
91
+ end
92
+
93
+ def to_h
94
+ {
95
+ id: @id,
96
+ safe: @safe,
97
+ value: @value,
98
+ }
99
+ end
100
+
101
+ def to_json
102
+ as_h = self.to_h
103
+
104
+ if as_h.respond_to? :to_json
105
+ as_h.to_json
106
+ else
107
+ nil
108
+ end
109
+ end
110
+
111
+ def failed?
112
+ @error != nil
113
+ end
114
+
115
+ def to_s
116
+ "<Mirador::Result; id: #{ @id }; safe: #{ @safe }; value: #{ @value }/>"
117
+ end
118
+
119
+ def name
120
+ @id
121
+ end
122
+
35
123
  end
36
124
 
37
125
  class Client
38
126
  include HTTParty
39
127
  base_uri 'api.mirador.im'
128
+
40
129
  default_timeout 10
41
130
 
42
- MAX_LEN = 8
131
+ MAX_LEN = 5
132
+ MAX_ID_LEN = 256
133
+ DATA_URI_PRE = ';base64,'
134
+ DATA_URI_PRELEN = 8
43
135
 
44
136
  def initialize(api_key)
45
137
  @options = { api_key: api_key }
46
138
  end
47
139
 
48
- def classify_urls urls
49
-
50
- if urls.length > MAX_LEN
51
- out = []
52
- urls.each_slice(MAX_LEN) do |s|
53
- out << self.classify_urls(s)
140
+ # metaprogramming extreme
141
+ [:url, :file, :buffer, :encoded_string, :data_uri].each do |datatype|
142
+ define_method("classify_#{datatype.to_s}s") do |args, params={}|
143
+ flexible_request args, params do |item|
144
+ fmt_items(datatype, item)
54
145
  end
146
+ end
55
147
 
56
- return out.flatten
148
+ define_method("classify_#{datatype.to_s}") do |args, params={}|
149
+ res = self.send("classify_#{datatype.to_s}s", args, params)
150
+ res[0]
57
151
  end
58
152
 
59
- res = self.class.post(
60
- "/v1/classify",
61
- {
62
- body: @options.merge({url: urls}),
63
- headers: {"User-Agent" => "Mirador Client v1.0/Ruby"}
64
- }
65
- )
153
+ end
66
154
 
67
- if res['errors']
68
- raise ApiError, res['errors']
69
- elsif not res
70
- raise ApiError, "no response: #{ res.code }"
71
- end
155
+ protected
156
+
157
+ def flexible_request(args, params={}, &cb)
158
+ req = {}
159
+
160
+ req = (if args.is_a? Hash
161
+
162
+ Hash[args.map do |k, v|
163
+ process_param(k, v)
164
+ end]
165
+
166
+ elsif args.is_a? String
167
+ Hash[[process_argument(args)]]
168
+
169
+ elsif args and args.length
170
+ Hash[args.each_with_index.map do |a, idx|
171
+ process_argument(a, idx)
172
+ end]
72
173
 
73
- return Result.parse_results urls, res['results']
174
+ elsif params
175
+ Hash[params.map do |k, v|
176
+ process_param(k, v)
177
+ end]
178
+ end)
179
+
180
+ chunked_request(req) do |item|
181
+ formatted = cb.call(item)
182
+ make_request(formatted)
183
+ end
74
184
  end
75
185
 
76
- def classify_files files
77
- if files.length > MAX_LEN
78
- out = []
79
- files.each_slice(MAX_LEN) do |s|
80
- out << self.classify_files(s)
186
+ def process_argument arg, idx=0
187
+
188
+ if arg.is_a?(String)
189
+ if arg.length < MAX_ID_LEN
190
+ [arg, arg]
191
+ else
192
+ [idx, arg]
81
193
  end
82
194
 
83
- return out.flatten
84
- end
195
+ elsif arg.respond_to?(:name) and arg.respond_to?(:read)
85
196
 
86
- processed = files.map do |f| self.process_file(f) end
87
- return self.classify_encoded files, processed
88
- end
197
+ [arg.name, arg]
198
+
199
+ elsif arg.respond_to?(:id) and arg.respond_to?(:data)
89
200
 
201
+ [arg.id, arg.data]
90
202
 
91
- def classify_raw_images imgs
203
+ elsif arg.is_a?(Hash)
92
204
 
93
- if imgs.length > MAX_LEN
94
- out = []
95
- imgs.each_slice(MAX_LEN) do |s|
96
- out << self.classify_raw_images(Hash[s])
205
+ if arg.has_key? :id and arg.has_key? :data
206
+ [arg[:id], arg[:data]]
207
+ elsif arg.has_key? 'id' and arg.has_key? 'data'
208
+ [arg['id'], arg['data']]
97
209
  end
98
210
 
99
- return out.flatten
211
+ else
212
+ raise ApiError, "Invalid argument: #{ arg }"
100
213
  end
101
214
 
102
- # expects a hash
103
- # id => image
104
- images, names = [], []
105
- imgs.each_pair do |k, v|
106
- images << v
107
- names << k
215
+ end
216
+
217
+ # given a parameter passed in,
218
+ # assuming that its a id => data mapping, return
219
+ # the correct formatting/check for any fuck ups
220
+ # @arguments:
221
+ # k - key
222
+ # v - value
223
+ # @returns:
224
+ # { k => v } pair
225
+ def process_param k, v
226
+
227
+ if v.is_a?(File)
228
+ [ k, v.read ]
229
+ elsif k.respond_to?(:to_s) and v.is_a?(String)
230
+ [ k.to_s, v ]
231
+ else
232
+ raise ApiError, "Invalid Argument: #{ k } => #{ v }"
233
+ end
234
+
235
+ end
236
+
237
+ # given a request and a block,
238
+ # call the block X number of times
239
+ # where X is request.length / MAX_LEN
240
+ def chunked_request req, &mthd
241
+ output = ResultList.new
242
+ req.each_slice(MAX_LEN).each do |slice|
243
+ output.update(mthd.call(slice))
108
244
  end
109
245
 
110
- processed = images.map { |i| Base64.encode64(i).gsub("\n", '') }
111
- return self.classify_encoded names, processed
246
+ return output
112
247
  end
113
248
 
114
- def process_file file
115
- data = File.read(file)
116
- Base64.encode64(data).gsub("\n", '')
249
+ # basically, transform hash h into a hash
250
+ # where the key-value pairs are all formatted
251
+ # by 'fmt-item' (should double the number of key-value
252
+ # pairs in the hash)
253
+ def fmt_items name, h
254
+ out = {}
255
+ h.each_with_index do |kv, idx|
256
+ out.update fmt_item(name, idx, kv[0], kv[1])
257
+ end
258
+ return out
117
259
  end
118
260
 
119
- def classify_encoded files, encoded
261
+ @@name_map = {
262
+ file: 'image',
263
+ buffer: 'image',
264
+ raw: 'image',
265
+ url: 'url',
266
+ encoded_string: 'image',
267
+ data_uri: 'image',
268
+ }
269
+
270
+ @@formatters = {
271
+ url: Proc.new { |url| url },
272
+
273
+ file: Proc.new { |file|
274
+
275
+ Base64.encode64(if file.respond_to? :read
276
+ file.read
277
+ else
278
+ File.read(file)
279
+ end).gsub(/\n/, '')
280
+
281
+ },
282
+
283
+ buffer: Proc.new { |file|
284
+
285
+ Base64.encode64(file).gsub(/\n/, '')
286
+
287
+ },
288
+
289
+ raw: Proc.new { |file|
290
+
291
+ Base64.encode64(file).gsub(/\n/, '')
292
+
293
+ },
294
+
295
+ encoded_string: Proc.new { |b64str|
296
+ b64str.gsub(/\n/, '')
297
+ },
298
+
299
+ data_uri: Proc.new { |datauri|
300
+ datauri.sub(/^.+;base64,/, '').gsub(/\n/,'')
301
+ },
302
+
303
+ }
304
+
305
+ # produce a k-v mapping internal to the API,
306
+ # so that 'name' is the datatype:
307
+ # e.g., name[idx][id], name[idx][data]
308
+ def fmt_item name, idx, id, data
309
+ formatted = @@formatters[name].call(data)
310
+ datatype = @@name_map[name]
311
+ {
312
+ "#{datatype}[#{idx}][id]" => id,
313
+ "#{datatype}[#{idx}][data]" => formatted,
314
+ }
315
+ end
316
+
317
+ # base method to actually make the request
318
+ def make_request params
319
+
120
320
  res = self.class.post(
121
321
  "/v1/classify",
122
322
  {
123
- body: @options.merge({image: encoded}),
124
- headers: {'User-Agent' => 'Mirador Client v1.0/Ruby'},
323
+ body: @options.merge(params),
324
+ headers: {"User-Agent" => "Mirador Client v1.0/Ruby"}
125
325
  }
126
326
  )
127
327
 
328
+ k = 'results'
329
+
128
330
  if res['errors']
129
- raise ApiError, res['errors']
130
- end
131
331
 
132
- if not res
133
- raise ApiError, "no response", res.code
332
+ if not res['result']
333
+ raise ApiError, res
334
+ else
335
+ k = 'result'
336
+ end
337
+
338
+ elsif not res
339
+ raise ApiError, "no response: #{ res.code }"
134
340
  end
135
341
 
136
- return Result.parse_results(files, res['results'])
342
+ return ResultList.parse_results res[k]
137
343
  end
138
344
 
139
345
  end
@@ -1,3 +1,3 @@
1
1
  module Mirador
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
data/test/test_mirador.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'test/unit'
2
- require 'mirador'
2
+ require './lib/mirador'
3
+ require 'base64'
3
4
 
4
5
  class MiradorTest < Test::Unit::TestCase
5
6
 
@@ -11,15 +12,14 @@ class MiradorTest < Test::Unit::TestCase
11
12
  SFW_URL = "http://demo.mirador.im/test/sfw.jpg"
12
13
  NSFW_URL = "http://demo.mirador.im/test/nsfw.jpg"
13
14
 
14
- MM = Mirador::Client.new('')
15
+ MM = Mirador::Client.new(ENV['MIRADOR_API_KEY'])
15
16
 
16
17
  def test_classify_files
17
18
 
18
19
  res = MM.classify_files([NSFW_IM, SFW_IM])
19
-
20
20
  assert_equal res.length, 2
21
21
 
22
- nsfw, sfw = res
22
+ nsfw, sfw = res[NSFW_IM], res[SFW_IM]
23
23
 
24
24
  assert_operator nsfw.value, :>=, 0.50
25
25
  assert_operator sfw.value, :<, 0.50
@@ -27,92 +27,147 @@ class MiradorTest < Test::Unit::TestCase
27
27
  assert nsfw.name.eql?(NSFW_IM), "nsfw name does not match"
28
28
  assert sfw.name.eql?(SFW_IM), "sfw name does not match"
29
29
 
30
+ assert nsfw.id.eql?(NSFW_IM)
31
+ assert sfw.id.eql?(SFW_IM)
32
+
30
33
  assert sfw.safe
31
34
  assert (not nsfw.safe)
32
35
 
33
36
  end
34
37
 
35
- def test_chunked_files
36
- nsfw_files = [NSFW_IM]*10
37
- sfw_files = [SFW_IM]*10
38
+ def test_classify_urls
38
39
 
39
- nres = MM.classify_files(nsfw_files)
40
- assert_equal nres.length, 10
40
+ res = MM.classify_urls([NSFW_URL, SFW_URL])
41
+ assert_equal 2, res.length
41
42
 
42
- nres.each do |r|
43
- assert_operator r.value, :>=, 0.50
44
- assert r.name.eql?(NSFW_IM)
45
- assert (not r.safe)
46
- end
43
+ nsfw, sfw = res[NSFW_URL], res[SFW_URL]
47
44
 
48
- sres = MM.classify_files(sfw_files)
49
- assert_equal sres.length, 10
45
+ assert_operator nsfw.value, :>=, 0.50
46
+ assert_operator sfw.value, :<, 0.50
47
+
48
+ assert nsfw.name.eql?(NSFW_URL), "nsfw name does not match"
49
+ assert sfw.name.eql?(SFW_URL), "sfw name does not match"
50
+
51
+ assert nsfw.id.eql?(NSFW_URL)
52
+ assert sfw.id.eql?(SFW_URL)
53
+
54
+ assert sfw.safe
55
+ assert (not nsfw.safe)
50
56
 
51
- sres.each do |r|
52
- assert_operator r.value, :<, 0.50
53
- assert r.name.eql?(SFW_IM)
54
- assert r.safe
55
- end
56
57
  end
57
58
 
58
- def test_chunked_urls
59
- nsfw_urls = [NSFW_URL]*10
60
- sfw_urls = [SFW_URL]*10
59
+ def test_classify_chunked_urls
61
60
 
62
- nres = MM.classify_urls(nsfw_urls)
63
- assert_equal nres.length, 10
61
+ r = Hash[([NSFW_URL]*10).each_with_index.map do |url, idx|
62
+ [ "#{ idx }-im", url ]
63
+ end]
64
64
 
65
- nres.each do |r|
66
- assert_not_nil r
67
- assert_operator r.value, :>=, 0.50
68
- assert r.name.eql?(NSFW_URL)
69
- assert (not r.safe)
70
- end
65
+ res = MM.classify_urls(r)
71
66
 
72
- sres = MM.classify_urls(sfw_urls)
73
- assert_equal sres.length, 10
67
+ assert_equal 10, res.length
74
68
 
75
- sres.each do |r|
76
- assert_not_nil r
77
- assert_not_nil r.value
78
- assert_operator r.value, :<, 0.50
79
- assert r.name.eql?(SFW_URL)
80
- assert r.safe
69
+ res.each do |id, r|
70
+ assert_operator r.value, :>=, 0.50
81
71
  end
82
72
 
83
73
  end
84
74
 
85
- def test_classify_urls
86
- res = MM.classify_urls([NSFW_URL, SFW_URL])
75
+ def test_hash_call
87
76
 
88
- assert_equal res.length, 2
89
- nsfw, sfw = res
77
+ res = MM.classify_urls(nsfw: NSFW_URL, sfw: SFW_URL)
78
+
79
+ assert res[:nsfw]
80
+ assert res[:sfw]
90
81
 
91
- assert nsfw.name.eql?(NSFW_URL)
92
- assert sfw.name.eql?(SFW_URL)
82
+ nsfw = res[:nsfw]
83
+ sfw = res[:sfw]
93
84
 
94
85
  assert_operator nsfw.value, :>=, 0.50
95
86
  assert_operator sfw.value, :<, 0.50
96
87
 
88
+ assert nsfw.name.eql?('nsfw'), "nsfw name does not match"
89
+ assert sfw.name.eql?('sfw'), "sfw name does not match"
90
+
91
+ assert nsfw.id.eql?('nsfw')
92
+ assert sfw.id.eql?('sfw')
93
+
97
94
  assert sfw.safe
98
95
  assert (not nsfw.safe)
96
+
99
97
  end
100
98
 
101
- def test_classify_raw
102
- nsfw_d, sfw_d = [NSFW_IM, SFW_IM].map { |f| File.read(f) }
103
- res = MM.classify_raw_images({ "nsfw" => nsfw_d, "sfw" => sfw_d })
99
+ def test_single_url
100
+
101
+ res = MM.classify_url(nsfw: NSFW_URL)
102
+ res1 = MM.classify_url(NSFW_URL)
103
+
104
+ assert_equal res.value, res1.value
105
+
106
+ end
107
+
108
+ def test_items_call
109
+
110
+ res = MM.classify_urls([{ id: :nsfw, data: NSFW_URL }, { id: :sfw, data: SFW_URL }])
104
111
 
105
112
  assert_equal res.length, 2
106
- nsfw, sfw = res
107
113
 
108
- assert nsfw.name.eql?('nsfw'), "invalid name: #{ nsfw.name }"
109
- assert sfw.name.eql?('sfw')
114
+ assert res[:nsfw]
115
+ assert res[:sfw]
110
116
 
111
- assert_operator nsfw.value, :>=, 0.50
112
- assert_operator sfw.value, :<, 0.50
117
+ end
118
+
119
+ def test_classify_buffers
120
+
121
+ bufs = Hash[([File.read(NSFW_IM)]*3).each_with_index.map do |b, idx|
122
+ ["#{idx}-buf", b]
123
+ end]
124
+
125
+
126
+ res = MM.classify_buffers(bufs)
127
+
128
+ res1 = MM.classify_buffer(File.read(SFW_IM))
129
+
130
+ assert_equal res.length, 3
131
+ assert_operator res1.value, :<=, 0.50
132
+
133
+ res.each do |r|
134
+ assert_operator r.value, :>=, 0.5
135
+ end
136
+ end
137
+
138
+ def test_data_uris
139
+ duri = 'data:image/jpg;base64,' + Base64.encode64(File.read(NSFW_IM)).gsub(/\n/, '')
140
+
141
+ res = MM.classify_data_uris(nsfw: duri)
142
+
143
+ assert res[:nsfw]
144
+ assert_operator res[:nsfw].value, :>=, 0.50
145
+ end
146
+
147
+ def test_encoded_string
148
+
149
+ tdata = Hash[[SFW_IM, NSFW_IM].map do |fname|
150
+ [fname, Base64.encode64(File.read(fname))]
151
+ end]
152
+
153
+ res = MM.classify_encoded_strings(tdata)
154
+
155
+ assert res[SFW_IM]
156
+ assert res[NSFW_IM]
157
+
158
+ assert_operator res[NSFW_IM].value, :>=, 0.50
159
+
160
+ end
161
+
162
+ def test_item_error
163
+
164
+ res = MM.classify_urls([{ id: :nsfw, data: 'invalid-url'}, { id: :sfw, data: SFW_URL }])
165
+
166
+ assert_equal res.length, 2
167
+ assert res[:sfw]
168
+
169
+ assert res.any? do |r| r.failed? end
113
170
 
114
- assert sfw.safe
115
- assert (not nsfw.safe)
116
171
  end
117
172
 
118
173
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mirador
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Jacob
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-17 00:00:00.000000000 Z
11
+ date: 2014-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty