voice_id 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.
@@ -0,0 +1,33 @@
1
+ require 'http'
2
+
3
+ module VoiceId
4
+ module RequestHelpers
5
+ class InvalidApiRequestError < StandardError; end
6
+
7
+ # generate body for content-type "multipart/form-data"
8
+ def create_body_for_enrollment(audio_file_path)
9
+ { :form => { :file => HTTP::FormData::File.new(audio_file_path) } }
10
+ end
11
+
12
+ def parse_error_response(response)
13
+ msg = response.parse["error"]["message"] || "request returned status #{response.code}"
14
+ raise InvalidApiRequestError, msg
15
+ end
16
+
17
+ def send_request(path, method, req_headers, body)
18
+ _headers = req_headers ? headers.merge(req_headers) : headers
19
+ _path = api_base_url + path
20
+ _req = HTTP.headers(_headers)
21
+
22
+ case method
23
+ when :Post
24
+ body ? _req.post(_path, body) : _req.post(_path)
25
+ when :Get
26
+ _req.get(_path)
27
+ when :Delete
28
+ _req.delete(_path)
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module VoiceId
2
+ module Utils
3
+ def get_operation_id(operation_url)
4
+ operation_url.match(/operations\/(.+)/)[1]
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,251 @@
1
+ module VoiceId
2
+ class Verification < VoiceId::Base
3
+
4
+ # params
5
+ # profile_id
6
+ # unique profile id
7
+ # audio_file_path
8
+ # path to the audio of speaker
9
+ #
10
+ # Microsoft API response
11
+ # 200 Response
12
+ # {
13
+ # "result" : "Accept", // [Accept | Reject]
14
+ # "confidence" : "Normal", // [Low | Normal | High]
15
+ # "phrase": "recognized phrase"
16
+ # }
17
+ #
18
+ #
19
+ # returns
20
+ # success
21
+ # verification results { Hash }
22
+ # fail (500 error)
23
+ # false
24
+ def verify_speaker(profile_id, audio_file_path)
25
+ _method = :Post
26
+ _path = "/verify?verificationProfileId=#{profile_id}"
27
+ _headers = { }
28
+ _body = create_body_for_enrollment(audio_file_path)
29
+ _response = send_request(_path, _method, _headers, _body)
30
+
31
+ _response.code == 200 ? _response.parse : parse_error_response(_response)
32
+ end
33
+
34
+ # Microsoft API response
35
+ # 200 Response
36
+ # {
37
+ # "verificationProfileId": "49a36324-fc4b-4387-aa06-090cfbf0064f",
38
+ # }
39
+ #
40
+ # 500 Response
41
+ # {
42
+ # "error":{
43
+ # "code" : "InternalServerError",
44
+ # "message" : "SpeakerInvalid",
45
+ # }
46
+ # }
47
+ #
48
+ # returns
49
+ # success
50
+ # new profileId { Hash }
51
+ # fail
52
+ # false (indicating new profile was not created)
53
+ def create_profile
54
+ super("/verificationProfiles")
55
+ end
56
+
57
+ # params
58
+ # profileId - required
59
+ # a valid id { String }
60
+ #
61
+ # Microsoft API response
62
+ # 200 Response
63
+ # "" (empty string)
64
+ #
65
+ # 500 Response
66
+ # {
67
+ # "error": {
68
+ # "code" : "InternalServerError",
69
+ # "message" : "SpeakerInvalid",
70
+ # }
71
+ # }
72
+ #
73
+ # returns
74
+ # success
75
+ # profile id that was deleted { String }
76
+ # fail
77
+ # false (indicating delete of id failed)
78
+ def delete_profile(profileId)
79
+ super("/verificationProfiles/#{profileId}")
80
+ end
81
+
82
+ # Microsoft API response
83
+ # 200 Response
84
+ # [{
85
+ # "verificationProfileId" : "111f427c-3791-468f-b709-fcef7660fff9",
86
+ # "locale" : "en-US",
87
+ # "enrollmentsCount" : 2,
88
+ # "remainingEnrollmentsCount" : 0,
89
+ # "createdDateTime" : "2015-04-23T18:25:43.511Z",
90
+ # "lastActionDateTime" : "2015-04-23T18:25:43.511Z",
91
+ # "enrollmentStatus" : "Enrolled" //[Enrolled | Enrolling | Training]
92
+ # }, …]
93
+ #
94
+ # 500 Response
95
+ # {
96
+ # "error": {
97
+ # "code" : "InternalServerError",
98
+ # "message" : "SpeakerInvalid",
99
+ # }
100
+ # }
101
+ #
102
+ # returns
103
+ # success
104
+ # A list of all the profiles { Array }
105
+ # fail
106
+ # false (indicating delete of id failed)
107
+ def get_all_profiles
108
+ super('/verificationProfiles')
109
+ end
110
+
111
+ # params
112
+ # profileId
113
+ # a valid profileId { String }
114
+ #
115
+ # Microsoft API response
116
+ # 200 Response
117
+ # {
118
+ # "verificationProfileId" : "111f427c-3791-468f-b709-fcef7660fff9",
119
+ # "locale" : "en-US",
120
+ # "enrollmentsCount" : 2,
121
+ # "remainingEnrollmentsCount" : 0,
122
+ # "createdDateTime" : "2015-04-23T18:25:43.511Z",
123
+ # "lastActionDateTime" : "2015-04-23T18:25:43.511Z",
124
+ # "enrollmentStatus" : "Enrolled" // [Enrolled | Enrolling | Training]
125
+ # }
126
+ #
127
+ # 500 Response
128
+ # {
129
+ # "error": {
130
+ # "code" : "InternalServerError",
131
+ # "message" : "SpeakerInvalid",
132
+ # }
133
+ # }
134
+ #
135
+ # returns
136
+ # success
137
+ # a profile { Hash }
138
+ # fail
139
+ # false (indicating delete of id failed)
140
+ def get_profile(profileId)
141
+ super("/verificationProfiles/#{profileId}")
142
+ end
143
+
144
+ # each speaker(profile) must provide at least 3 enrollments to the api
145
+ # params
146
+ # profileId
147
+ # a valid profileId { String }
148
+ # audio_file_path
149
+ # path to the audio file { String }
150
+ # audio requirments => Wav, PCM, 16k rate, 16 bit sample rate, mono
151
+ #
152
+ # Microsoft API response
153
+ # 200 Response
154
+ # {
155
+ # "enrollmentStatus" : "Enrolled", // [Enrolled | Enrolling | Training]
156
+ # "enrollmentsCount":0,
157
+ # "remainingEnrollments" : 0,
158
+ # "phrase" : "Recognized verification phrase"
159
+ # }
160
+ #
161
+ # 500 Response
162
+ # {
163
+ # "error": {
164
+ # "code" : "InternalServerError",
165
+ # "message" : "SpeakerInvalid",
166
+ # }
167
+ # }
168
+ #
169
+ # returns
170
+ # success
171
+ # enrollment details { Hash }
172
+ # fail
173
+ # false
174
+ def create_enrollment(profileId, audio_file_path)
175
+ _method = :Post
176
+ _path = "/verificationProfiles/#{profileId}/enroll"
177
+ _headers = { }
178
+ _body = create_body_for_enrollment(audio_file_path)
179
+ _response = send_request(_path, _method, _headers, _body)
180
+
181
+ _response.code == 200 ? _response.parse : parse_error_response(_response)
182
+ end
183
+
184
+ # params
185
+ # profileId - required
186
+ # a valid id { String }
187
+ #
188
+ # Microsoft API response
189
+ # 200 Response
190
+ # "" (empty string)
191
+ #
192
+ # 500 Response
193
+ # {
194
+ # "error": {
195
+ # "code" : "InternalServerError",
196
+ # "message" : "SpeakerInvalid",
197
+ # }
198
+ # }
199
+ #
200
+ # returns
201
+ # success
202
+ # profile id that was deleted { String }
203
+ # fail
204
+ # false (indicating delete of enrollments failed)
205
+ def reset_all_enrollments_for_profile(profileId)
206
+ super("/verificationProfiles/#{profileId}/reset")
207
+ end
208
+
209
+ # params
210
+ # locale - required
211
+ # a valid query string (used to return phrases in selected language)
212
+ #
213
+ # Microsoft API response
214
+ # 200 Response
215
+ # [
216
+ # {"phrase"=>"i am going to make him an offer he cannot refuse"},
217
+ # {"phrase"=>"houston we have had a problem"},
218
+ # {"phrase"=>"my voice is my passport verify me"},
219
+ # {"phrase"=>"apple juice tastes funny after toothpaste"},
220
+ # {"phrase"=>"you can get in without your password"},
221
+ # {"phrase"=>"you can activate security system now"},
222
+ # {"phrase"=>"my voice is stronger than passwords"},
223
+ # {"phrase"=>"my password is not your business"},
224
+ # {"phrase"=>"my name is unknown to you"},
225
+ # {"phrase"=>"be yourself everyone else is already taken"}
226
+ # ]
227
+ #
228
+ # 500 Response
229
+ # {
230
+ # "error": {
231
+ # "code" : "InternalServerError",
232
+ # "message" : "SpeakerInvalid",
233
+ # }
234
+ # }
235
+ #
236
+ # returns
237
+ # success
238
+ # list of acceptable phrases { Array }
239
+ # fail
240
+ # false
241
+ def list_all_verification_phrases
242
+ _method = :Get
243
+ _locale = "en-us"
244
+ _path = "/verificationPhrases?locale=#{_locale}"
245
+ _headers = { "Content-Type" => "application/json" }
246
+ _response = send_request(_path, _method, _headers, nil)
247
+
248
+ _response.code == 200 ? _response.parse : parse_error_response(_response)
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,95 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe VoiceId::Identification do
4
+
5
+ @profile = {
6
+ "identificationProfileId" => "111f427c-3791-468f-b709-fcef7660fff9",
7
+ "locale" => "en-US",
8
+ "enrollmentSpeechTime" => 0.0,
9
+ "remainingEnrollmentSpeechTime" => 0.0,
10
+ "createdDateTime" => "2015-04-23T18:25:43.511Z",
11
+ "lastActionDateTime" => "2015-04-23T18:25:43.511Z",
12
+ "enrollmentStatus" => "Enrolled"
13
+ }
14
+
15
+ before :each do
16
+ @identification = VoiceId::Identification.new("some_api_key")
17
+ @identification.api_base_url = "http://localhost:11988"
18
+ end
19
+
20
+
21
+ describe "#create_profile" do
22
+ it "should be able to create a new profile and return the details" do
23
+ expect(@identification.create_profile).to eql(@identification_profile)
24
+ end
25
+ end
26
+
27
+ describe "#delete_profile" do
28
+ it "should delete a profile and return true" do
29
+ profileId = "12345678"
30
+ expect(@identification.delete_profile(profileId)).to eql(true)
31
+ end
32
+ end
33
+
34
+ describe "#get_all_profiles" do
35
+ it "should return an array of all profiles" do
36
+ expect(@identification.get_all_profiles).to eql(@identification_profiles)
37
+ end
38
+ end
39
+
40
+ describe "#get_profile" do
41
+ it "should return a profile hash" do
42
+ profileId = "987654321"
43
+ expect(@identification.get_profile(profileId)).to eql(@profile)
44
+ end
45
+ end
46
+
47
+ describe "#create_enrollment" do
48
+ it "should create a new enrollment for a profile and return an operation url" do
49
+ data = { :form => { :file => "cool.wav" } }
50
+ profileId = "0991883883"
51
+ shortAudio = true
52
+ allow_any_instance_of(VoiceId::RequestHelpers).to receive(:create_body_for_enrollment).and_return(data)
53
+ expect(@identification.create_enrollment(profileId , shortAudio, '/path/to/some/audio_file.wav')).to eql("https://www.coolsite/operations/123456789")
54
+ end
55
+ end
56
+
57
+ describe "#reset_all_enrollments_for_profile" do
58
+ it "should return true if all enrollments for a profile was deleted" do
59
+ profileId = "0991883883"
60
+ expect(@identification.reset_all_enrollments_for_profile(profileId)).to eql(true)
61
+ end
62
+ end
63
+
64
+ describe "#get_operation_id" do
65
+ it "returns an operation id" do
66
+ operation_url = "https://api.projectoxford.ai/spid/v1.0/operations/995a8745-0098-4c12-9889-bad14859y7a4"
67
+ expect(@identification.get_operation_id(operation_url)).to eql("995a8745-0098-4c12-9889-bad14859y7a4")
68
+ end
69
+ end
70
+
71
+ describe "#identify_speaker" do
72
+ it "identifies a speaker using any audio snippet" do
73
+ data = { :form => { :file => "cool.wav" } }
74
+ profile_ids = ["profile_id1", "profile_id2"]
75
+ short_audio = true
76
+ audio_file_path = '/path/to/some/audio_file.wav'
77
+ allow_any_instance_of(VoiceId::RequestHelpers).to receive(:create_body_for_enrollment).and_return(data)
78
+ expect(@identification.identify_speaker(profile_ids, short_audio, audio_file_path)).to eql("https://www.coolsite/operations/123456789")
79
+ end
80
+ end
81
+
82
+ describe "#get_operation_status(operationId)" do
83
+ it "returns operation status" do
84
+ operation_id = "1234567890"
85
+ expect(@identification.get_operation_status(operation_id)).to eql(@operation_status)
86
+ end
87
+ end
88
+
89
+ describe "#parse_error_response" do
90
+ it "throws an error" do
91
+ @identification.api_base_url = "http://localhost:11988/error"
92
+ expect { @identification.create_profile }.to raise_error(VoiceId::RequestHelpers::InvalidApiRequestError, "oh no everything's going wrong today!")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,122 @@
1
+ json_type = { "content-type" => "application/json" }
2
+ form_data_type = { "content-type" => "multipart/form-data"}
3
+ enrollment_operation = { "Operation-Location" => "https://www.coolsite/operations/123456789" }
4
+
5
+ @new_identification_profile = {
6
+ "identificationProfileId" => "49a36324-fc4b-4387-aa06-090cfbf0064f",
7
+ }
8
+
9
+ @identification_profile = {
10
+ "identificationProfileId" => "111f427c-3791-468f-b709-fcef7660fff9",
11
+ "locale" => "en-US",
12
+ "enrollmentSpeechTime" => 0.0,
13
+ "remainingEnrollmentSpeechTime" => 0.0,
14
+ "createdDateTime" => "2015-04-23T18:25:43.511Z",
15
+ "lastActionDateTime" => "2015-04-23T18:25:43.511Z",
16
+ "enrollmentStatus" => "Enrolled"
17
+ }
18
+
19
+ @identification_profiles = [
20
+ {
21
+ "identificationProfileId" => "111f427c-3791-468f-b709-fcef7660fff9",
22
+ "locale" => "en-US",
23
+ "enrollmentSpeechTime" => 0.0,
24
+ "remainingEnrollmentSpeechTime" => 0.0,
25
+ "createdDateTime" => "2015-04-23T18:25:43.511Z",
26
+ "lastActionDateTime" => "2015-04-23T18:25:43.511Z",
27
+ "enrollmentStatus" => "Enrolled"
28
+ }
29
+ ]
30
+
31
+ @new_verification_profile = {
32
+ "verificationProfileId" => "49a36324-fc4b-4387-aa06-090cfbf0064f",
33
+ }
34
+
35
+ @verification_profiles = [
36
+ {
37
+ "verificationProfileId" => "111f427c-3791-468f-b709-fcef7660fff9",
38
+ "locale" => "en-US",
39
+ "enrollmentsCount" => 2,
40
+ "remainingEnrollmentsCount" => 0,
41
+ "createdDateTime" => "2015-04-23T18:25:43.511Z",
42
+ "lastActionDateTime" => "2015-04-23T18:25:43.511Z",
43
+ "enrollmentStatus" => "Enrolled"
44
+ }
45
+ ]
46
+
47
+ @verification_profile = {
48
+ "verificationProfileId" => "111f427c-3791-468f-b709-fcef7660fff9",
49
+ "locale" => "en-US",
50
+ "enrollmentsCount" => 2,
51
+ "remainingEnrollmentsCount" => 0,
52
+ "createdDateTime" => "2015-04-23T18:25:43.511Z",
53
+ "lastActionDateTime" => "2015-04-23T18:25:43.511Z",
54
+ "enrollmentStatus" => "Enrolled"
55
+ }
56
+
57
+ @all_verification_phrases = [
58
+ {"phrase" => "i am going to make him an offer he cannot refuse"},
59
+ {"phrase" => "houston we have had a problem"},
60
+ {"phrase" => "my voice is my passport verify me"},
61
+ {"phrase" => "apple juice tastes funny after toothpaste"},
62
+ {"phrase" => "you can get in without your password"},
63
+ {"phrase" => "you can activate security system now"},
64
+ {"phrase" => "my voice is stronger than passwords"},
65
+ {"phrase" => "my password is not your business"},
66
+ {"phrase" => "my name is unknown to you"},
67
+ {"phrase" => "be yourself everyone else is already taken"}
68
+ ]
69
+
70
+ @verification_enrollment_response = {
71
+ "enrollmentStatus" => "Enrolling",
72
+ "enrollmentsCount" => 1,
73
+ "remainingEnrollments" => 2,
74
+ "phrase" => "i am going to make him an offer he cannot refuse"
75
+ }
76
+
77
+ @verify_speaker_result = {
78
+ "result" => "Accept",
79
+ "confidence" => "High",
80
+ "phrase" => "i am going to make him an offer he cannot refuse"
81
+ }
82
+
83
+ @operation_status = {
84
+ "status" => "succeeded",
85
+ "createdDateTime" => "2016-09-20T01:51:39.134487Z",
86
+ "lastActionDateTime" => "2016-09-20T01:51:41.4183611Z",
87
+ "processingResult" => {
88
+ "enrollmentStatus" => "Enrolled",
89
+ "remainingEnrollmentSpeechTime" => 0.0,
90
+ "speechTime" => 7.93, # useful speech duration
91
+ "enrollmentSpeechTime" => 31.72
92
+ }
93
+ }
94
+
95
+ error_response = {
96
+ "error" => {
97
+ "message" => "oh no everything's going wrong today!"
98
+ }
99
+ }
100
+
101
+ Mimic.mimic do
102
+ post("/error/identificationProfiles").returning(error_response.to_json, 400, json_type)
103
+ post("/error/verificationProfiles").returning(error_response.to_json, 400, json_type)
104
+
105
+ get("/identificationProfiles").returning(@identification_profiles.to_json, 200, json_type)
106
+ get("/operations/:operationId").returning(@operation_status.to_json, 200, json_type)
107
+ get("/identificationProfiles/:profileId").returning(@identification_profile.to_json, 200, json_type)
108
+ post("/identificationProfiles").returning(@new_identification_profile.to_json, 200, json_type)
109
+ post("/identificationProfiles/:profileId/reset").returning("".to_json, 200, json_type)
110
+ post("/identificationProfiles/:profileId/enroll").returning("".to_json, 202, json_type.merge(enrollment_operation))
111
+ post("/identify").returning("".to_json, 202, json_type.merge(enrollment_operation))
112
+ delete("/identificationProfiles/:profileId").returning("".to_json, 200, json_type)
113
+
114
+ get("/verificationProfiles").returning(@verification_profiles.to_json, 200, json_type)
115
+ get("/verificationProfiles/:profileId").returning(@verification_profile.to_json, 200, json_type)
116
+ get("/verificationPhrases").returning(@all_verification_phrases.to_json, 200, json_type)
117
+ post("/verificationProfiles").returning(@new_verification_profile.to_json, 200, json_type)
118
+ post("/verificationProfiles/:profileId/reset").returning("".to_json, 200, json_type)
119
+ post("/verificationProfiles/:profileId/enroll").returning(@verification_enrollment_response.to_json, 200, json_type)
120
+ post("/verify").returning(@verify_speaker_result, 200, json_type)
121
+ delete("/verificationProfiles/:profileId").returning("".to_json, 200, json_type)
122
+ end