mirador 0.0.4 → 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 +8 -8
- data/README.md +231 -16
- data/lib/mirador.rb +277 -71
- data/lib/mirador/version.rb +1 -1
- data/test/test_mirador.rb +111 -56
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MGZmMTU1ODJkZDdiZjM3MmQ1NzA1MjJkNjkxMjUxMzM1MmMyOTQ1Yg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
OWYyNjc4N2EzMWU1Mjc4YWUxMWJmOGU4NjNhMmFkZjRiZWUzYWYwMA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
Mjk2MWU1MDM2ZmNiZDUyYTMwMTRlNzIyMTMyZWY5ZDg5ZDdmNGZhM2FhNDlk
|
10
|
+
MDk5MmQ4Nzg3MTI1MjE2ODM1MDY4MjU5YzEzNjAyZmRhYTEwYThmMjI2MTVj
|
11
|
+
Mjk1N2IxMGIyNzdhMmQxY2IyN2U3ZGQxZGQxOGRlZmI2ZDAyNDU=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
23
|
+
## Mirador::Result and Mirador::ResultList
|
24
24
|
|
25
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
45
|
-
|
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
|
-
###
|
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
|
-
|
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
|
-
|
6
|
+
class ApiError < StandardError
|
7
|
+
end
|
7
8
|
|
8
|
-
class
|
9
|
-
|
9
|
+
class ResultList
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
def initialize(items=[])
|
13
|
+
@items = {}
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@value = data['value']
|
15
|
+
items.each do |x|
|
16
|
+
@items[x.id] = x
|
17
|
+
end
|
15
18
|
end
|
16
19
|
|
17
|
-
def
|
18
|
-
|
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
|
40
|
+
def update other
|
41
|
+
@items.update(other)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h
|
45
|
+
@items
|
46
|
+
end
|
22
47
|
|
23
|
-
|
24
|
-
|
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
|
-
|
28
|
-
|
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
|
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 =
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
84
|
-
end
|
195
|
+
elsif arg.respond_to?(:name) and arg.respond_to?(:read)
|
85
196
|
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
203
|
+
elsif arg.is_a?(Hash)
|
92
204
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
211
|
+
else
|
212
|
+
raise ApiError, "Invalid argument: #{ arg }"
|
100
213
|
end
|
101
214
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
111
|
-
return self.classify_encoded names, processed
|
246
|
+
return output
|
112
247
|
end
|
113
248
|
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
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(
|
124
|
-
headers: {
|
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
|
-
|
133
|
-
|
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
|
342
|
+
return ResultList.parse_results res[k]
|
137
343
|
end
|
138
344
|
|
139
345
|
end
|
data/lib/mirador/version.rb
CHANGED
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
|
36
|
-
nsfw_files = [NSFW_IM]*10
|
37
|
-
sfw_files = [SFW_IM]*10
|
38
|
+
def test_classify_urls
|
38
39
|
|
39
|
-
|
40
|
-
assert_equal
|
40
|
+
res = MM.classify_urls([NSFW_URL, SFW_URL])
|
41
|
+
assert_equal 2, res.length
|
41
42
|
|
42
|
-
|
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
|
-
|
49
|
-
|
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
|
59
|
-
nsfw_urls = [NSFW_URL]*10
|
60
|
-
sfw_urls = [SFW_URL]*10
|
59
|
+
def test_classify_chunked_urls
|
61
60
|
|
62
|
-
|
63
|
-
|
61
|
+
r = Hash[([NSFW_URL]*10).each_with_index.map do |url, idx|
|
62
|
+
[ "#{ idx }-im", url ]
|
63
|
+
end]
|
64
64
|
|
65
|
-
|
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
|
-
|
73
|
-
assert_equal sres.length, 10
|
67
|
+
assert_equal 10, res.length
|
74
68
|
|
75
|
-
|
76
|
-
|
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
|
86
|
-
res = MM.classify_urls([NSFW_URL, SFW_URL])
|
75
|
+
def test_hash_call
|
87
76
|
|
88
|
-
|
89
|
-
|
77
|
+
res = MM.classify_urls(nsfw: NSFW_URL, sfw: SFW_URL)
|
78
|
+
|
79
|
+
assert res[:nsfw]
|
80
|
+
assert res[:sfw]
|
90
81
|
|
91
|
-
|
92
|
-
|
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
|
102
|
-
|
103
|
-
res = MM.
|
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
|
109
|
-
assert sfw
|
114
|
+
assert res[:nsfw]
|
115
|
+
assert res[:sfw]
|
110
116
|
|
111
|
-
|
112
|
-
|
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
|
+
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-
|
11
|
+
date: 2014-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|