Permalink
Browse files

Merge branch 'feature/nlsml' into develop

  • Loading branch information...
benlangfeld committed Mar 2, 2013
2 parents f3d5b03 + bffb25d commit c2217f3a92a0df3ccd22e1f374cfdd5244d7a7d4
View
@@ -1,4 +1,5 @@
# [develop](https://github.com/benlangfeld/ruby_speech)
+ * Feature: NLSML building & parsing
# [1.0.2](https://github.com/benlangfeld/ruby_speech/compare/v1.0.1...v1.0.2) - [2012-12-26](https://rubygems.org/gems/ruby_speech/versions/1.0.2)
* Bugfix: Get test suite passing on JRuby
View
118 README.md
@@ -1,10 +1,12 @@
# RubySpeech
-RubySpeech is a library for constructing and parsing Text to Speech (TTS) and Automatic Speech Recognition (ASR) documents such as [SSML](http://www.w3.org/TR/speech-synthesis) and [GRXML](http://www.w3.org/TR/speech-grammar/). The primary use case is for construction of these documents to be processed by TTS and ASR engines.
+RubySpeech is a library for constructing and parsing Text to Speech (TTS) and Automatic Speech Recognition (ASR) documents such as [SSML](http://www.w3.org/TR/speech-synthesis), [GRXML](http://www.w3.org/TR/speech-grammar/) and [NLSML](http://www.w3.org/TR/nl-spec/). Such documents can be constructed to be processed by TTS and ASR engines, parsed as the result from such, or used in the implementation of such engines.
## Installation
gem install ruby_speech
## Library
+
+### SSML
RubySpeech provides a DSL for constructing SSML documents like so:
```ruby
@@ -45,6 +47,7 @@ Once your `Speak` is fully prepared and you're ready to send it off for processi
You may also then need to call `to_s`.
+### GRXML
Construct a GRXML (SRGS) document like this:
@@ -103,7 +106,7 @@ which becomes
</grammar>
```
-### Grammar matching
+#### Grammar matching
It is possible to match some arbitrary input against a GRXML grammar. In order to do so, certain normalization routines should first be run on the grammar in order to prepare it for matching. These are reference inlining, tokenization and whitespace normalization, and are described [in the SRGS spec](http://www.w3.org/TR/speech-grammar/#S2.1). This process will transform the above grammar like so:
@@ -198,6 +201,111 @@ Matching against some sample input strings then returns the following results:
=> #<RubySpeech::GRXML::NoMatch:0x00000101371660>
```
+### NLSML
+
+[Natural Language Semantics Markup Language](http://www.w3.org/TR/nl-spec/) is the format used by many Speech Recognition engines and natural language processors to add semantic information to human language. RubySpeech is capable of generating and parsing such documents.
+
+It is possible to generate an NLSML document like so:
+
+```ruby
+require 'ruby_speech'
+
+nlsml = RubySpeech::NLSML.draw(grammar: 'http://flight', 'xmlns:myApp' => 'foo') do
+ interpretation confidence: 0.6 do
+ input "I want to go to Pittsburgh", mode: :speech
+
+ model do
+ group name: 'airline' do
+ string name: 'to_city'
+ end
+ end
+
+ instance do
+ self['myApp'].airline do
+ to_city 'Pittsburgh'
+ end
+ end
+ end
+
+ interpretation confidence: 0.4 do
+ input "I want to go to Stockholm"
+
+ model do
+ group name: 'airline' do
+ string name: 'to_city'
+ end
+ end
+
+ instance do
+ self['myApp'].airline do
+ to_city "Stockholm"
+ end
+ end
+ end
+end
+
+nlsml.to_s
+```
+
+becomes:
+
+```xml
+<?xml version="1.0"?>
+<result xmlns:myApp="foo" xmlns:xf="http://www.w3.org/2000/xforms" grammar="http://flight">
+ <interpretation confidence="60">
+ <input mode="speech">I want to go to Pittsburgh</input>
+ <xf:model>
+ <xf:group name="airline">
+ <xf:string name="to_city"/>
+ </xf:group>
+ </xf:model>
+ <xf:instance>
+ <myApp:airline>
+ <myApp:to_city>Pittsburgh</myApp:to_city>
+ </myApp:airline>
+ </xf:instance>
+ </interpretation>
+ <interpretation confidence="40">
+ <input>I want to go to Stockholm</input>
+ <xf:model>
+ <xf:group name="airline">
+ <xf:string name="to_city"/>
+ </xf:group>
+ </xf:model>
+ <xf:instance>
+ <myApp:airline>
+ <myApp:to_city>Stockholm</myApp:to_city>
+ </myApp:airline>
+ </xf:instance>
+ </interpretation>
+</result>
+```
+
+It's also possible to parse an NLSML document and extract useful information from it. Taking the above example, one may do:
+
+```ruby
+document = RubySpeech.parse nlsml.to_s
+
+document.match? # => true
+document.interpretations # => [
+ {
+ confidence: 0.6,
+ input: { mode: :speech, content: 'I want to go to Pittsburgh' },
+ instance: { airline: { to_city: 'Pittsburgh' } }
+ },
+ {
+ confidence: 0.4,
+ input: { content: 'I want to go to Stockholm' },
+ instance: { airline: { to_city: 'Stockholm' } }
+ }
+ ]
+document.best_interpretation # => {
+ confidence: 0.6,
+ input: { mode: :speech, content: 'I want to go to Pittsburgh' },
+ instance: { airline: { to_city: 'Pittsburgh' } }
+ }
+```
+
Check out the [YARD documentation](http://rdoc.info/github/benlangfeld/ruby_speech/master/frames) for more
## Features:
@@ -226,6 +334,10 @@ Check out the [YARD documentation](http://rdoc.info/github/benlangfeld/ruby_spee
* `<tag/>`
* `<token/>`
+### NLSML
+* Document construction
+* Simple data extraction from documents
+
## TODO:
### SSML
* `<lexicon/>`
@@ -236,11 +348,11 @@ Check out the [YARD documentation](http://rdoc.info/github/benlangfeld/ruby_spee
* `<example/>`
* `<lexicon/>`
-
## Links:
* [Source](https://github.com/benlangfeld/ruby_speech)
* [Documentation](http://rdoc.info/gems/ruby_speech/frames)
* [Bug Tracker](https://github.com/benlangfeld/ruby_speech/issues)
+* [CI](https://travis-ci.org/#!/benlangfeld/ruby_speech)
## Note on Patches/Pull Requests
View
@@ -15,8 +15,22 @@ module RubySpeech
autoload :GenericElement
autoload :SSML
autoload :GRXML
+ autoload :NLSML
autoload :XML
end
+
+ def self.parse(string)
+ document = Nokogiri::XML.parse string, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS
+ namespace = document.root.namespace
+ case namespace && namespace.href
+ when SSML::SSML_NAMESPACE
+ SSML::Element.import string
+ when GRXML::GRXML_NAMESPACE
+ GRXML::Element.import string
+ when NLSML::NLSML_NAMESPACE, nil
+ NLSML::Document.new document
+ end
+ end
end
ActiveSupport::Autoload.eager_autoload!
View
@@ -0,0 +1,19 @@
+module RubySpeech
+ module NLSML
+ extend ActiveSupport::Autoload
+
+ NLSML_NAMESPACE = 'http://www.w3c.org/2000/11/nlsml'
+ XFORMS_NAMESPACE = 'http://www.w3.org/2000/xforms'
+
+ eager_autoload do
+ autoload :Builder
+ autoload :Document
+ end
+
+ def self.draw(options = {}, &block)
+ Builder.new(options, &block).document
+ end
+ end
+end
+
+ActiveSupport::Autoload.eager_autoload!
@@ -0,0 +1,47 @@
+module RubySpeech
+ module NLSML
+ class Builder
+ attr_reader :document
+
+ def initialize(options = {}, &block)
+ options = {'xmlns' => NLSML_NAMESPACE, 'xmlns:xf' => XFORMS_NAMESPACE}.merge(options)
+ @document = Nokogiri::XML::Builder.new do |builder|
+ builder.result options do |r|
+ apply_block r, &block
+ end
+ end.doc
+ end
+
+ def interpretation(*args, &block)
+ if args.last.respond_to?(:has_key?) && args.last.has_key?(:confidence)
+ args.last[:confidence] = (args.last[:confidence] * 100).to_i
+ end
+ @result.send :interpretation, *args, &block
+ end
+
+ def model(*args, &block)
+ xf_namespaced_element :model, *args, &block
+ end
+
+ def instance(*args, &block)
+ xf_namespaced_element :instance, *args, &block
+ end
+
+ def method_missing(method_name, *args, &block)
+ @result.send method_name, *args, &block
+ end
+
+ private
+
+ def apply_block(result, &block)
+ @result = result
+ instance_eval &block
+ end
+
+ def xf_namespaced_element(element_name, *args, &block)
+ namespace = @result.send :[], 'xf'
+ namespace.send element_name, &block
+ end
+ end
+ end
+end
@@ -0,0 +1,116 @@
+require 'delegate'
+
+module RubySpeech
+ module NLSML
+ class Document < SimpleDelegator
+ def initialize(xml)
+ super
+ @xml = xml
+ end
+
+ def grammar
+ result['grammar']
+ end
+
+ def interpretations
+ interpretation_nodes.map do |interpretation|
+ interpretation_hash_for_interpretation interpretation
+ end
+ end
+
+ def best_interpretation
+ interpretation_hash_for_interpretation interpretation_nodes.first
+ end
+
+ def match?
+ interpretation_nodes.count > 0 && !nomatch? && !noinput?
+ end
+
+ def ==(other)
+ to_xml == other.to_xml
+ end
+
+ def noinput?
+ noinput_elements.any?
+ end
+
+ private
+
+ def nomatch?
+ nomatch_elements.count >= input_elements.count
+ end
+
+ def nomatch_elements
+ result.xpath 'ns:interpretation/ns:input/ns:nomatch|interpretation/input/nomatch', 'ns' => NLSML_NAMESPACE
+ end
+
+ def noinput_elements
+ result.xpath 'ns:interpretation/ns:input/ns:noinput|interpretation/input/noinput', 'ns' => NLSML_NAMESPACE
+ end
+
+ def input_elements
+ result.xpath 'ns:interpretation/ns:input|interpretation/input', 'ns' => NLSML_NAMESPACE
+ end
+
+ def input_hash_for_interpretation(interpretation)
+ input_element = interpretation.at_xpath '(ns:input|input)', 'ns' => NLSML_NAMESPACE
+ { content: input_element.content }.tap do |h|
+ h[:mode] = input_element['mode'].to_sym if input_element['mode']
+ end
+ end
+
+ def instance_hash_for_interpretation(interpretation)
+ instances = instance_elements interpretation
+ return unless instances.any?
+ element_children_key_value instances.first
+ end
+
+ def instances_collection_for_interpretation(interpretation)
+ instances = instance_elements interpretation
+ instances.map do |instance|
+ element_children_key_value instance
+ end
+ end
+
+ def instance_elements(interpretation)
+ interpretation.xpath '(xf:instance|ns:instance|instance)', 'xf' => XFORMS_NAMESPACE, 'ns' => NLSML_NAMESPACE
+ end
+
+ def element_children_key_value(element)
+ element.children.inject({}) do |acc, child|
+ acc[child.node_name.to_sym] = case child.children.count
+ when 0
+ child.content
+ when 1
+ if child.children.first.is_a?(Nokogiri::XML::Text)
+ child.children.first.content
+ else
+ element_children_key_value child
+ end
+ else
+ element_children_key_value child
+ end
+ acc
+ end
+ end
+
+ def interpretation_hash_for_interpretation(interpretation)
+ {
+ confidence: interpretation['confidence'].to_f/100,
+ input: input_hash_for_interpretation(interpretation),
+ instance: instance_hash_for_interpretation(interpretation),
+ instances: instances_collection_for_interpretation(interpretation)
+ }
+ end
+
+ def result
+ root
+ end
+
+ def interpretation_nodes
+ nodes = result.xpath '(ns:interpretation|interpretation)', 'ns' => NLSML_NAMESPACE
+ nodes.sort_by { |int| -int[:confidence].to_i }
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit c2217f3

Please sign in to comment.