ibm_watson 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +258 -0
  3. data/bin/console +14 -0
  4. data/bin/setup +8 -0
  5. data/lib/ibm_watson.rb +16 -0
  6. data/lib/ibm_watson/assistant_v1.rb +1997 -0
  7. data/lib/ibm_watson/detailed_response.rb +21 -0
  8. data/lib/ibm_watson/discovery_v1.rb +2039 -0
  9. data/lib/ibm_watson/iam_token_manager.rb +166 -0
  10. data/lib/ibm_watson/language_translator_v3.rb +411 -0
  11. data/lib/ibm_watson/natural_language_classifier_v1.rb +309 -0
  12. data/lib/ibm_watson/natural_language_understanding_v1.rb +297 -0
  13. data/lib/ibm_watson/personality_insights_v3.rb +260 -0
  14. data/lib/ibm_watson/speech_to_text_v1.rb +2153 -0
  15. data/lib/ibm_watson/text_to_speech_v1.rb +716 -0
  16. data/lib/ibm_watson/tone_analyzer_v3.rb +287 -0
  17. data/lib/ibm_watson/version.rb +3 -0
  18. data/lib/ibm_watson/visual_recognition_v3.rb +579 -0
  19. data/lib/ibm_watson/watson_api_exception.rb +41 -0
  20. data/lib/ibm_watson/watson_service.rb +180 -0
  21. data/lib/ibm_watson/websocket/recognize_callback.rb +32 -0
  22. data/lib/ibm_watson/websocket/speech_to_text_websocket_listener.rb +162 -0
  23. data/rakefile +45 -0
  24. data/test/integration/test_assistant_v1.rb +645 -0
  25. data/test/integration/test_discovery_v1.rb +200 -0
  26. data/test/integration/test_iam_assistant_v1.rb +707 -0
  27. data/test/integration/test_language_translator_v3.rb +81 -0
  28. data/test/integration/test_natural_language_classifier_v1.rb +69 -0
  29. data/test/integration/test_natural_language_understanding_v1.rb +98 -0
  30. data/test/integration/test_personality_insights_v3.rb +95 -0
  31. data/test/integration/test_speech_to_text_v1.rb +187 -0
  32. data/test/integration/test_text_to_speech_v1.rb +81 -0
  33. data/test/integration/test_tone_analyzer_v3.rb +72 -0
  34. data/test/integration/test_visual_recognition_v3.rb +64 -0
  35. data/test/test_helper.rb +22 -0
  36. data/test/unit/test_assistant_v1.rb +1598 -0
  37. data/test/unit/test_discovery_v1.rb +1144 -0
  38. data/test/unit/test_iam_token_manager.rb +165 -0
  39. data/test/unit/test_language_translator_v3.rb +461 -0
  40. data/test/unit/test_natural_language_classifier_v1.rb +187 -0
  41. data/test/unit/test_natural_language_understanding_v1.rb +132 -0
  42. data/test/unit/test_personality_insights_v3.rb +172 -0
  43. data/test/unit/test_speech_to_text_v1.rb +755 -0
  44. data/test/unit/test_text_to_speech_v1.rb +336 -0
  45. data/test/unit/test_tone_analyzer_v3.rb +200 -0
  46. data/test/unit/test_vcap_using_personality_insights.rb +150 -0
  47. data/test/unit/test_visual_recognition_v3.rb +345 -0
  48. metadata +302 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("json")
4
+ # Custom exception class for errors returned from Watson APIs
5
+ class WatsonApiException < StandardError
6
+ attr_reader :code, :error, :info, :transaction_id, :global_transaction_id
7
+ # :param HTTP::Response response: The response object from the Watson API
8
+ def initialize(code: nil, error: nil, info: nil, transaction_id: nil, global_transaction_id: nil, response: nil)
9
+ if code.nil? || error.nil?
10
+ @code = response.code
11
+ @error = response.reason
12
+ unless response.body.empty?
13
+ body_hash = JSON.parse(response.body.to_s)
14
+ @code = body_hash["code"] || body_hash["error_code"]
15
+ @error = body_hash["error"] || body_hash["error_message"]
16
+ %w[code error_code error error_message].each { |k| body_hash.delete(k) }
17
+ @info = body_hash
18
+ end
19
+ @transaction_id = transaction_id
20
+ @global_transaction_id = global_transaction_id
21
+ @transaction_id = response.headers["X-DP-Watson-Tran-ID"] if response.headers.include?("X-DP-Watson-Tran-ID")
22
+ @global_transaction_id = response.headers["X-Global-Transaction-ID"] if response.headers.include?("X-Global-Transaction-ID")
23
+ else
24
+ # :nocov:
25
+ @code = code
26
+ @error = error
27
+ @info = info
28
+ @transaction_id = transaction_id
29
+ @global_transaction_id = global_transaction_id
30
+ # :nocov:
31
+ end
32
+ end
33
+
34
+ def to_s
35
+ msg = "Error: #{@error}, Code: #{@code}"
36
+ msg += ", Information: #{@info}" unless @info.nil?
37
+ msg += ", X-dp-watson-tran-id: #{@transaction_id}" unless @transaction_id.nil?
38
+ msg += ", X-global-transaction-id: #{@global_transaction_id}" unless @global_transaction_id.nil?
39
+ msg
40
+ end
41
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("http")
4
+ require("rbconfig")
5
+ require("stringio")
6
+ require("json")
7
+ require_relative("./detailed_response.rb")
8
+ require_relative("./watson_api_exception.rb")
9
+ require_relative("./iam_token_manager.rb")
10
+ require_relative("./version.rb")
11
+ # require("httplog")
12
+ # HttpLog.configure do |config|
13
+ # config.log_connect = true
14
+ # config.log_request = true
15
+ # config.log_headers = true
16
+ # config.log_data = true
17
+ # config.log_status = true
18
+ # config.log_response = true
19
+ # end
20
+
21
+ # Class for interacting with the Watson API
22
+ class WatsonService
23
+ attr_accessor :password, :url, :username
24
+ attr_reader :conn
25
+ def initialize(vars)
26
+ defaults = {
27
+ vcap_services_name: nil,
28
+ username: nil,
29
+ password: nil,
30
+ use_vcap_services: true,
31
+ api_key: nil,
32
+ x_watson_learning_opt_out: false,
33
+ iam_api_key: nil,
34
+ iam_access_token: nil,
35
+ iam_url: nil
36
+ }
37
+ vars = defaults.merge(vars)
38
+ @url = vars[:url]
39
+ @username = nil
40
+ @password = nil
41
+ @token_manager = nil
42
+ @temp_headers = nil
43
+
44
+ user_agent_string = "watson-apis-ruby-sdk-" + IBMWatson::VERSION
45
+ user_agent_string += " #{RbConfig::CONFIG["host"]}"
46
+ user_agent_string += " #{RbConfig::CONFIG["RUBY_BASE_NAME"]}-#{RbConfig::CONFIG["RUBY_PROGRAM_VERSION"]}"
47
+
48
+ headers = {
49
+ "User-Agent" => user_agent_string
50
+ }
51
+ headers["x-watson-learning-opt-out"] = true if vars[:x_watson_learning_opt_out]
52
+ if vars[:use_vcap_services]
53
+ @vcap_service_credentials = load_from_vcap_services(service_name: vars[:vcap_services_name])
54
+ if !@vcap_service_credentials.nil? && @vcap_service_credentials.instance_of?(Hash)
55
+ @url = @vcap_service_credentials["url"]
56
+ @username = @vcap_service_credentials["username"] if @vcap_service_credentials.key?("username")
57
+ @password = @vcap_service_credentials["password"] if @vcap_service_credentials.key?("password")
58
+ @iam_api_key = @vcap_service_credentials["iam_api_key"] if @vcap_service_credentials.key?("iam_api_key")
59
+ @iam_access_token = @vcap_service_credentials["iam_access_token"] if @vcap_service_credentials.key?("iam_access_token")
60
+ @iam_url = @vcap_service_credentials["iam_url"] if @vcap_service_credentials.key?("iam_url")
61
+ end
62
+ end
63
+
64
+ if !vars[:iam_access_token].nil? || !vars[:iam_api_key].nil?
65
+ _token_manager(iam_api_key: vars[:iam_api_key], iam_access_token: vars[:iam_access_token], iam_url: vars[:iam_url])
66
+ elsif !vars[:username].nil? && !vars[:password].nil?
67
+ if vars[:username] == "apikey"
68
+ _iam_api_key(iam_api_key: vars[:password])
69
+ else
70
+ @username = vars[:username]
71
+ @password = vars[:password]
72
+ end
73
+ end
74
+
75
+ @conn = HTTP::Client.new(
76
+ headers: headers
77
+ ).timeout(
78
+ :per_operation,
79
+ read: 60,
80
+ write: 60,
81
+ connect: 60
82
+ )
83
+ end
84
+
85
+ def load_from_vcap_services(service_name:)
86
+ vcap_services = ENV["VCAP_SERVICES"]
87
+ unless vcap_services.nil?
88
+ services = JSON.parse(vcap_services)
89
+ return services[service_name][0]["credentials"] if services.key?(service_name)
90
+ end
91
+ nil
92
+ end
93
+
94
+ def add_default_headers(headers: {})
95
+ raise TypeError unless headers.instance_of?(Hash)
96
+ headers.each_pair { |k, v| @conn.default_options.headers.add(k, v) }
97
+ end
98
+
99
+ def _token_manager(iam_api_key: nil, iam_access_token: nil, iam_url: nil)
100
+ @iam_api_key = iam_api_key
101
+ @iam_access_token = iam_access_token
102
+ @iam_url = iam_url
103
+ @token_manager = IAMTokenManager.new(iam_api_key: iam_api_key, iam_access_token: iam_access_token, iam_url: iam_url)
104
+ end
105
+
106
+ def _iam_access_token(iam_access_token:)
107
+ @token_manager._access_token(iam_access_token: iam_access_token) unless @token_manager.nil?
108
+ @token_manager = IAMTokenManager.new(iam_access_token: iam_access_token) if @token_manager.nil?
109
+ @iam_access_token = iam_access_token
110
+ end
111
+
112
+ def _iam_api_key(iam_api_key:)
113
+ @token_manager._iam_api_key(iam_api_key: iam_api_key) unless @token_manager.nil?
114
+ @token_manager = IAMTokenManager.new(iam_api_key: iam_api_key) if @token_manager.nil?
115
+ @iam_api_key = iam_api_key
116
+ end
117
+
118
+ # @return [DetailedResponse]
119
+ def request(args)
120
+ defaults = { method: nil, url: nil, accept_json: false, headers: nil, params: nil, json: {}, data: nil }
121
+ args = defaults.merge(args)
122
+ args[:data].delete_if { |_k, v| v.nil? } if args[:data].instance_of?(Hash)
123
+ args[:json] = args[:data].merge(args[:json]) if args[:data].respond_to?(:merge)
124
+ args[:json] = args[:data] if args[:json].empty? || (args[:data].instance_of?(String) && !args[:data].empty?)
125
+ args[:json].delete_if { |_k, v| v.nil? } if args[:json].instance_of?(Hash)
126
+ args[:headers]["Accept"] = "application/json" if args[:accept_json]
127
+ args[:headers]["Content-Type"] = "application/json" unless args[:headers].key?("Content-Type")
128
+ args[:json] = args[:json].to_json if args[:json].instance_of?(Hash)
129
+ args[:headers].delete_if { |_k, v| v.nil? } if args[:headers].instance_of?(Hash)
130
+ args[:params].delete_if { |_k, v| v.nil? } if args[:params].instance_of?(Hash)
131
+ args[:form].delete_if { |_k, v| v.nil? } if args.key?(:form)
132
+ args.delete_if { |_, v| v.nil? }
133
+ args[:headers].delete("Content-Type") if args.key?(:form) || args[:json].nil?
134
+
135
+ if @username == "apikey"
136
+ _iam_api_key(iam_api_key: @password)
137
+ @username = nil
138
+ end
139
+
140
+ conn = @conn
141
+ if !@token_manager.nil?
142
+ access_token = @token_manager._token
143
+ args[:headers]["Authorization"] = "Bearer #{access_token}"
144
+ elsif !@username.nil? && !@password.nil?
145
+ conn = @conn.basic_auth(user: @username, pass: @password)
146
+ end
147
+
148
+ args[:headers] = args[:headers].merge(@temp_headers) unless @temp_headers.nil?
149
+ @temp_headers = nil unless @temp_headers.nil?
150
+
151
+ if args.key?(:form)
152
+ response = conn.follow.request(
153
+ args[:method],
154
+ HTTP::URI.parse(@url + args[:url]),
155
+ headers: conn.default_options.headers.merge(HTTP::Headers.coerce(args[:headers])),
156
+ params: args[:params],
157
+ form: args[:form]
158
+ )
159
+ else
160
+ response = conn.follow.request(
161
+ args[:method],
162
+ HTTP::URI.parse(@url + args[:url]),
163
+ headers: conn.default_options.headers.merge(HTTP::Headers.coerce(args[:headers])),
164
+ body: args[:json],
165
+ params: args[:params]
166
+ )
167
+ end
168
+ return DetailedResponse.new(response: response) if (200..299).cover?(response.code)
169
+ raise WatsonApiException.new(response: response)
170
+ end
171
+
172
+ # @note Chainable
173
+ # @param headers [Hash] Custom headers to be sent with the request
174
+ # @return [self]
175
+ def headers(headers)
176
+ raise TypeError("Expected Hash type, received #{headers.class}") unless headers.instance_of?(Hash)
177
+ @temp_headers = headers
178
+ self
179
+ end
180
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IBMWatson
4
+ # Abstract class for Recognize Callbacks
5
+ class RecognizeCallback
6
+ def initialize(*); end
7
+
8
+ # Called when an interim result is received
9
+ def on_transcription(transcript:); end
10
+
11
+ # Called when a WebSocket connection is made
12
+ def on_connected; end
13
+
14
+ # Called when there is an error in the WebSocket connection
15
+ def on_error(error:); end
16
+
17
+ # Called when there is an inactivity timeout
18
+ def on_inactivity_timeout(error:); end
19
+
20
+ # Called when the service is listening for audio
21
+ def on_listening; end
22
+
23
+ # Called after the service returns the final result for the transcription
24
+ def on_transcription_complete; end
25
+
26
+ # Called when the service returns the final hypothesis
27
+ def on_hypothesis(hypothesis:); end
28
+
29
+ # Called when the service returns results. The data is returned unparsed
30
+ def on_data(data:); end
31
+ end
32
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("eventmachine")
4
+ require("faye/websocket")
5
+ require("json")
6
+
7
+ ONE_KB = 1024
8
+ TIMEOUT_PREFIX = "No speech detected for".freeze
9
+ CLOSE_SIGNAL = 1000
10
+ TEN_MILLISECONDS = 0.01
11
+
12
+ # Class for interacting with the WebSocket API
13
+ class WebSocketClient
14
+ def initialize(audio: nil, chunk_data:, options:, recognize_callback:, url:, headers:)
15
+ @audio = audio
16
+ @options = options
17
+ @callback = recognize_callback
18
+ @bytes_sent = 0
19
+ @headers = headers
20
+ @is_listening = false
21
+ @url = url
22
+ @timer = nil
23
+ @chunk_data = chunk_data
24
+ @mic_running = false
25
+ @data_size = audio.nil? ? 0 : @audio.size
26
+ @queue = Queue.new
27
+ end
28
+
29
+ def start
30
+ on_open = lambda do |event|
31
+ on_connect(event)
32
+ @client.send(build_start_message(options: @options))
33
+ @mic_running = true if @chunk_data
34
+ send_audio(data: @audio)
35
+ end
36
+
37
+ on_message = lambda do |event|
38
+ json_object = JSON.parse(event.data)
39
+ if json_object.key?("error")
40
+ error = json_object["error"]
41
+ if error.start_with?(TIMEOUT_PREFIX)
42
+ @callback.on_inactivity_timeout(error: error)
43
+ else
44
+ @callback.on_error(error: error)
45
+ end
46
+ elsif json_object.key?("state")
47
+ if !@is_listening
48
+ @is_listening = true
49
+ else
50
+ @client.send(build_close_message)
51
+ @callback.on_transcription_complete
52
+ @client.close(CLOSE_SIGNAL)
53
+ end
54
+ elsif json_object.key?("results") || json_object.key?("speaker_labels")
55
+ hypothesis = ""
56
+ unless json_object["results"].nil?
57
+ hypothesis = json_object.dig("results", 0, "alternatives", 0, "transcript")
58
+ b_final = json_object.dig("results", 0, "final")
59
+ transcripts = extract_transcripts(alternatives: json_object.dig("results", 0, "alternatives"))
60
+
61
+ @callback.on_hypothesis(hypothesis: hypothesis) if b_final
62
+
63
+ @callback.on_transcription(transcript: transcripts)
64
+ @callback.on_data(data: json_object)
65
+ end
66
+ end
67
+ end
68
+
69
+ on_close = lambda do |_event|
70
+ @client = nil
71
+ EM.stop_event_loop
72
+ end
73
+
74
+ on_error = lambda do |event|
75
+ p event.message
76
+ end
77
+
78
+ EM.reactor_thread.join unless EM.reactor_thread.nil?
79
+ EM.run do
80
+ @client = Faye::WebSocket::Client.new(@url, nil, headers: @headers)
81
+ @client.onclose = on_close
82
+ @client.onerror = on_error
83
+ @client.onmessage = on_message
84
+ @client.onopen = on_open
85
+ @client.add_listener(Faye::WebSocket::API::Event.create("open"))
86
+ @client.add_listener(Faye::WebSocket::API::Event.create("message"))
87
+ @client.add_listener(Faye::WebSocket::API::Event.create("close"))
88
+ @client.add_listener(Faye::WebSocket::API::Event.create("error"))
89
+ end
90
+ end
91
+
92
+ def add_audio_chunk(chunk:)
93
+ @data_size += chunk.size
94
+ @queue << chunk
95
+ end
96
+
97
+ def stop_audio
98
+ @mic_running = false
99
+ end
100
+
101
+ private
102
+
103
+ def on_connect(_response)
104
+ @callback.on_connected
105
+ end
106
+
107
+ def build_start_message(options:)
108
+ options["action"] = "start"
109
+ options.to_json
110
+ end
111
+
112
+ def build_close_message
113
+ { "action" => "close" }.to_json
114
+ end
115
+
116
+ def send_audio(data:)
117
+ if @chunk_data
118
+ if @mic_running
119
+ @queue.empty? ? send_chunk(chunk: nil, final: false) : send_chunk(chunk: @queue.pop(true), final: false)
120
+ elsif @queue.length == 1
121
+ send_chunk(chunk: @queue.pop(true), final: true)
122
+ @queue.close
123
+ @timer.cancel if @timer.respond_to?(:cancel)
124
+ return
125
+ else
126
+ send_chunk(chunk: @queue.pop(true), final: false) unless @queue.empty?
127
+ end
128
+ else
129
+ if @bytes_sent + ONE_KB >= @data_size
130
+ if @data_size > @bytes_sent
131
+ send_chunk(chunk: data.read(ONE_KB), final: true)
132
+ @timer.cancel if @timer.respond_to?(:cancel)
133
+ return
134
+ end
135
+ @timer.cancel if @timer.respond_to?(:cancel)
136
+ end
137
+ send_chunk(chunk: data.read(ONE_KB), final: false)
138
+ end
139
+ @timer = EventMachine::Timer.new(TEN_MILLISECONDS) { send_audio(data: data) }
140
+ end
141
+
142
+ def extract_transcripts(alternatives:)
143
+ transcripts = []
144
+ unless alternatives.nil?
145
+ alternatives.each do |alternative|
146
+ transcript = {}
147
+ transcript["confidence"] = alternative["confidence"] if alternative.key?("confidence")
148
+ transcript["transcript"] = alternative["transcript"]
149
+ transcripts << transcript
150
+ end
151
+ end
152
+ transcripts
153
+ end
154
+
155
+ def send_chunk(chunk:, final: false)
156
+ return if chunk.nil?
157
+ @bytes_sent += chunk.size
158
+ @client.send(chunk.bytes)
159
+ @client.send({ "action" => "stop" }.to_json) if final
160
+ @timer.cancel if @timer.respond_to?(:cancel) && final
161
+ end
162
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv/tasks"
4
+ require "rake/testtask"
5
+ require "rubocop/rake_task"
6
+
7
+ task default: %w[def]
8
+
9
+ RuboCop::RakeTask.new
10
+
11
+ namespace :test do
12
+ Rake::TestTask.new do |t|
13
+ t.name = "unit"
14
+ t.description = "Run unit tests"
15
+ t.libs << "test"
16
+ t.test_files = FileList["test/unit/*.rb"]
17
+ t.verbose = true
18
+ t.warning = true
19
+ end
20
+
21
+ Rake::TestTask.new do |t|
22
+ t.name = "integration"
23
+ t.description = "Run integration tests (put credentials in a .env file)"
24
+ t.libs << "test"
25
+ t.test_files = FileList["test/integration/*.rb"]
26
+ t.verbose = true
27
+ t.warning = true
28
+ t.deps = [:dotenv]
29
+ end
30
+ end
31
+
32
+ desc "Run unit & integration tests"
33
+ task :test do
34
+ Rake::Task["test:unit"].invoke
35
+ Rake::Task["test:integration"].invoke
36
+ end
37
+
38
+ desc "Run tests and generate a code coverage report"
39
+ task :coverage do
40
+ ENV["COVERAGE"] = "true"
41
+ Rake::Task["test"].execute
42
+ end
43
+
44
+ task def: %i[rubocop coverage] do
45
+ end