dictation 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/dictation +5 -0
- data/lib/dictation/commander.rb +97 -0
- data/lib/dictation/control_center.rb +121 -0
- data/lib/dictation/operating_system.rb +43 -0
- data/lib/dictation/serializer.rb +45 -0
- data/lib/dictation/teacher.rb +35 -0
- data/lib/dictation/tts.rb +46 -0
- data/lib/dictation/typewriter.rb +62 -0
- data/lib/dictation/word.rb +49 -0
- data/lib/dictation.rb +1 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea842ecf7f99848898ad32ac6accae61d57e59ba
|
4
|
+
data.tar.gz: 75f245d58bdfe3b456d8cda2d09327e9668a5c45
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 664b01f3d7b97636e2c985428b3641dc184bfea53552b37a89730da128a4d170699482c05317010834e7e835114848835883b36581a680fa2691c995e7d7383a
|
7
|
+
data.tar.gz: 36072fec46ad89922e5c797fd8d95a86ccbb938709cbf32787c7ab029f0234741e35a9dd0b2993af98c44bd39970e09aa3f2b3106277959c7d103eb21e32e659
|
data/bin/dictation
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Dictation
|
4
|
+
class Commander
|
5
|
+
class << self
|
6
|
+
def order(args)
|
7
|
+
options = {}
|
8
|
+
commands = {
|
9
|
+
'new' => OptionParser.new do |opts|
|
10
|
+
opts.banner = set_banner('new')
|
11
|
+
|
12
|
+
opts.on('-d', '--dictate TTS', 'Specify two-letters language code for dictation') do |tts|
|
13
|
+
options[:dictate] = tts.downcase
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on('-v', '--verify TTS', 'Specify two-letters language code for verification') do |tts|
|
17
|
+
options[:verify] = tts.downcase
|
18
|
+
end
|
19
|
+
end,
|
20
|
+
|
21
|
+
'add' => OptionParser.new do |opts|
|
22
|
+
opts.banner = set_banner('add')
|
23
|
+
|
24
|
+
opts.on('-l', '--language LANGUAGE', 'Specify the target language file for adding more words') do |language|
|
25
|
+
options[:language] = language
|
26
|
+
end
|
27
|
+
end,
|
28
|
+
|
29
|
+
'dictate' => OptionParser.new do |opts|
|
30
|
+
opts.banner = set_banner('dictate')
|
31
|
+
|
32
|
+
opts.on('-l', '--language LANGUAGE', 'Specify the language for dictation') do |language|
|
33
|
+
options[:language] = language
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('-b', '--begin WORD', 'Begin with given word') do |word|
|
37
|
+
options[:word_begin] = word
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('-e', '--end WORD', 'End with given word') do |word|
|
41
|
+
options[:word_end] = word
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on('-s', '--start LINE', Integer, 'Start with given line') do |line|
|
45
|
+
options[:line_start] = line
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-f', '--finish LINE', Integer, 'Finish with given line') do |line|
|
49
|
+
options[:line_finish] = line
|
50
|
+
end
|
51
|
+
end,
|
52
|
+
|
53
|
+
'verify' => OptionParser.new do |opts|
|
54
|
+
opts.banner = set_banner('verify')
|
55
|
+
|
56
|
+
opts.on('-l', '--language LANGUAGE', 'Specify the language for dictation') do |language|
|
57
|
+
options[:language] = language
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on('-b', '--begin WORD', 'Begin with given word') do |word|
|
61
|
+
options[:word_begin] = word
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on('-e', '--end WORD', 'End with given word') do |word|
|
65
|
+
options[:word_end] = word
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on('-s', '--start LINE', Integer, 'Start with given line') do |line|
|
69
|
+
options[:line_start] = line
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on('-f', '--finish LINE', Integer, 'Finish with given line') do |line|
|
73
|
+
options[:line_finish] = line
|
74
|
+
end
|
75
|
+
end
|
76
|
+
}
|
77
|
+
valid_cmds = ['new', 'add', 'dictate', 'verify']
|
78
|
+
if valid_cmds.include?(args[0])
|
79
|
+
commands[args.shift].order!
|
80
|
+
options
|
81
|
+
else
|
82
|
+
puts "Unknown commands [#{args[0]}], only support #{valid_cmds.inspect}."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_caller_file_name
|
87
|
+
caller[-1].match(/\S+\.rb/)
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_banner(cmd)
|
91
|
+
"Usage: #{get_caller_file_name} #{cmd} [options]"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private_class_method :new, :get_caller_file_name, :set_banner
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Dictation
|
2
|
+
class ControlCenter
|
3
|
+
class << self
|
4
|
+
def launch
|
5
|
+
if OperatingSystem.supported?
|
6
|
+
command = ARGV[0]
|
7
|
+
options = Commander.order(ARGV)
|
8
|
+
case command
|
9
|
+
when 'new'
|
10
|
+
execute_new(options)
|
11
|
+
when 'add'
|
12
|
+
execute_add(options)
|
13
|
+
when 'dictate'
|
14
|
+
execute_dictate(options)
|
15
|
+
when 'verify'
|
16
|
+
execute_verify(options)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
puts 'Sorry, your Operating System is not supported. Only Mac OSX has built-in TTS (Text-to-Speech).'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute_new(options)
|
24
|
+
if options[:dictate] && options[:verify]
|
25
|
+
File.open("dict_#{options[:dictate]}_#{options[:verify]}.txt", "w") {}
|
26
|
+
else
|
27
|
+
puts 'Please give both dictate (-d) and verify (-v) languages.'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute_add(options)
|
32
|
+
if options[:language]
|
33
|
+
dictionary = get_dictionary(options[:language])
|
34
|
+
if dictionary
|
35
|
+
words = Typewriter.new.collect
|
36
|
+
Serializer.serialize(words, dictionary)
|
37
|
+
else
|
38
|
+
puts 'Can not find any dictionary file.'
|
39
|
+
end
|
40
|
+
else
|
41
|
+
puts 'Please specify the target language (-l).'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute_dictate(options)
|
46
|
+
execute_speak(options, :dictates)
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute_verify(options)
|
50
|
+
execute_speak(options, :verifies)
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_dictionary(language)
|
54
|
+
dictionary = nil
|
55
|
+
regex = /dict_#{language}_[a-z]{2}\.txt/
|
56
|
+
Dir.glob("*").each do |file|
|
57
|
+
(dictionary = file) && break if file.match(regex)
|
58
|
+
end
|
59
|
+
dictionary
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_languages(dictionary)
|
63
|
+
regex = /dict_([a-z]{2})_([a-z]{2})\.txt/
|
64
|
+
md = dictionary.match(regex)
|
65
|
+
[md[1], md[2]]
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_words(options)
|
69
|
+
words = []
|
70
|
+
dictionary = nil
|
71
|
+
language = options[:language]
|
72
|
+
word_begin = options[:word_begin]
|
73
|
+
word_end = options[:word_end]
|
74
|
+
line_start = options[:line_start]
|
75
|
+
line_finish = options[:line_finish]
|
76
|
+
|
77
|
+
if language
|
78
|
+
dictionary = get_dictionary(language)
|
79
|
+
end
|
80
|
+
|
81
|
+
if dictionary && (word_begin || line_start)
|
82
|
+
if word_end
|
83
|
+
words = Serializer.deserialize_a_portion_by_given_word(dictionary, word_begin, word_end)
|
84
|
+
elsif line_finish
|
85
|
+
words = Serializer.deserialize_a_portion_by_given_line(dictionary, line_start, line_finish)
|
86
|
+
else
|
87
|
+
words = (word_begin ? Serializer.deserialize_a_portion_by_given_word(dictionary, word_begin) : Serializer.deserialize_a_portion_by_given_line(dictionary, line_start))
|
88
|
+
end
|
89
|
+
else
|
90
|
+
words = Serializer.deserialize(dictionary)
|
91
|
+
end
|
92
|
+
|
93
|
+
words
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_teacher(dictionary)
|
97
|
+
languages = get_languages(dictionary)
|
98
|
+
tts_dictate = TTS.new(languages[0].intern)
|
99
|
+
tts_verify = TTS.new(languages[1].intern)
|
100
|
+
Teacher.new(tts_dictate, tts_verify)
|
101
|
+
end
|
102
|
+
|
103
|
+
def execute_speak(options, action)
|
104
|
+
if options.has_key?(:language)
|
105
|
+
words = get_words(options)
|
106
|
+
dictionary = get_dictionary(options[:language])
|
107
|
+
if dictionary
|
108
|
+
teacher = set_teacher(dictionary)
|
109
|
+
teacher.send(action, words)
|
110
|
+
else
|
111
|
+
puts 'Please check if the given language is correct (two-letters country code) or whether the dictionary file exists.'
|
112
|
+
end
|
113
|
+
else
|
114
|
+
puts 'Please specify target learning language by two-letters country code (-l)'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private_class_method :new, :get_dictionary, :get_languages, :get_words, :set_teacher, :execute_speak, :execute_new, :execute_add, :execute_dictate, :execute_verify
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Dictation
|
2
|
+
class OperatingSystem
|
3
|
+
TYPE = {
|
4
|
+
lin: 'linux',
|
5
|
+
mac: 'osx',
|
6
|
+
win: 'windows',
|
7
|
+
unknown: 'UNKNOWN'
|
8
|
+
}
|
9
|
+
|
10
|
+
SUPPORTED_TYPE = [TYPE[:mac]]
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def match_os(regex)
|
14
|
+
!!RUBY_PLATFORM.match(regex)
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_linux?
|
18
|
+
match_os(/linux/)
|
19
|
+
end
|
20
|
+
|
21
|
+
def is_mac?
|
22
|
+
match_os(/darwin/)
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_windows?
|
26
|
+
match_os(/mswin|mingw|cygwin|bccwin|wince|emx/)
|
27
|
+
end
|
28
|
+
|
29
|
+
def what
|
30
|
+
return TYPE[:lin] if is_linux?
|
31
|
+
return TYPE[:mac] if is_mac?
|
32
|
+
return TYPE[:win] if is_windows?
|
33
|
+
return TYPE[:unknown]
|
34
|
+
end
|
35
|
+
|
36
|
+
def supported?
|
37
|
+
SUPPORTED_TYPE.include?(what)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private_class_method :new, :match_os, :is_linux?, :is_mac?, :is_windows?, :what
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Dictation
|
2
|
+
class Serializer
|
3
|
+
class << self
|
4
|
+
def serialize(data, file)
|
5
|
+
if Array === data
|
6
|
+
data = data.inject('') do |chunk, word|
|
7
|
+
chunk += word.to_json + "\n"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
File.open(file, 'a') { |f| f.write(data) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(file)
|
14
|
+
words = []
|
15
|
+
File.open(file).each do |line|
|
16
|
+
words.push(JSON.parse(line))
|
17
|
+
end
|
18
|
+
words
|
19
|
+
end
|
20
|
+
|
21
|
+
def deserialize_a_portion_by_given_word(file, start, stop = nil)
|
22
|
+
all_words = deserialize(file)
|
23
|
+
idx_start = all_words.index { |x| x.value == start }
|
24
|
+
idx_stop = ( stop.nil? ? nil : all_words.index { |x| x.value == stop } )
|
25
|
+
idx_stop = all_words.size if idx_stop.nil?
|
26
|
+
if !idx_start.nil? && idx_start <= idx_stop
|
27
|
+
return all_words[idx_start..idx_stop]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def deserialize_a_portion_by_given_line(file, start, stop = nil)
|
32
|
+
all_words = deserialize(file)
|
33
|
+
unless stop.nil?
|
34
|
+
if stop >= start && stop <= all_words.size
|
35
|
+
return all_words[start-1..stop-1]
|
36
|
+
end
|
37
|
+
else
|
38
|
+
if start <= all_words.size
|
39
|
+
return all_words[start-1..-1]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Dictation
|
2
|
+
class Teacher
|
3
|
+
SLEEP_INTERVAL_WRITE_LETTER = 0.8
|
4
|
+
SLEEP_INTERVAL_READ_LETTER = 0.1
|
5
|
+
SLEEP_INTERVAL_GENERAL = 0.5
|
6
|
+
|
7
|
+
def initialize(tts_dictate, tts_verify)
|
8
|
+
@tts_dictate = tts_dictate
|
9
|
+
@tts_verify = tts_verify
|
10
|
+
end
|
11
|
+
|
12
|
+
def dictates(words)
|
13
|
+
words.each do |word|
|
14
|
+
vocabulary = word.value
|
15
|
+
@tts_dictate.speak(vocabulary)
|
16
|
+
sleep(vocabulary.length * SLEEP_INTERVAL_WRITE_LETTER)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def verifies(words)
|
21
|
+
words.each do |word|
|
22
|
+
@tts_dictate.speak(word.value)
|
23
|
+
sleep(SLEEP_INTERVAL_GENERAL)
|
24
|
+
word.orthography.each do |letter|
|
25
|
+
@tts_dictate.speak(letter)
|
26
|
+
sleep(SLEEP_INTERVAL_READ_LETTER)
|
27
|
+
end
|
28
|
+
sleep(SLEEP_INTERVAL_GENERAL)
|
29
|
+
@tts_verify.speak(word.translation)
|
30
|
+
puts word.value + ' ' + word.translation
|
31
|
+
sleep(SLEEP_INTERVAL_GENERAL)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dictation
|
2
|
+
class TTS
|
3
|
+
def initialize(language, voice = nil)
|
4
|
+
set_language_and_voice(language, voice)
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_available_language_and_voice_pairs
|
8
|
+
list_of_available_voices = `say -v '?'`.split("\n")
|
9
|
+
languages_and_voices = list_of_available_voices.inject({}) do |collection, record|
|
10
|
+
matched_results = record.match(/^(.*[^\s])\s+([a-z]{2})_[A-Z]{2}\s+/)
|
11
|
+
available_language = matched_results[2].downcase.intern
|
12
|
+
available_voice = matched_results[1]
|
13
|
+
if collection.has_key?(available_language)
|
14
|
+
collection[available_language] << available_voice
|
15
|
+
else
|
16
|
+
collection[available_language] = [available_voice]
|
17
|
+
end
|
18
|
+
collection
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_language_and_voice(language, voice)
|
23
|
+
system_languages_and_voices = get_available_language_and_voice_pairs
|
24
|
+
@language = language
|
25
|
+
if system_languages_and_voices.keys.include?(@language)
|
26
|
+
if voice
|
27
|
+
if system_languages_and_voices[@language].include?(voice)
|
28
|
+
@voice = voice
|
29
|
+
else
|
30
|
+
raise("No available voice found, please check if you have downloaded voice [#{voice}] in System Preferences -> Speech")
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@voice = system_languages_and_voices[@language].first
|
34
|
+
end
|
35
|
+
else
|
36
|
+
raise("No available language found, please check if you have any voice for [#{language}] in System Preferences -> Speech")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def speak(text)
|
41
|
+
system("say -v #{@voice} #{text} > /dev/null 2>&1")
|
42
|
+
end
|
43
|
+
|
44
|
+
private :get_available_language_and_voice_pairs, :set_language_and_voice
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Dictation
|
2
|
+
class Typewriter
|
3
|
+
attr_accessor :words
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@words = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_input_if_not_nil
|
10
|
+
input = gets
|
11
|
+
input.nil? ? nil : input.chomp!
|
12
|
+
end
|
13
|
+
|
14
|
+
def record
|
15
|
+
paint_print('Word: ', :blue)
|
16
|
+
value = get_input_if_not_nil
|
17
|
+
paint_print('Translation: ', :yellow)
|
18
|
+
translation = get_input_if_not_nil
|
19
|
+
@words.push(Word.new(value, translation)) if value && translation
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect
|
23
|
+
paint_puts('Please type word and translation one by one, press Enter key to save. When you finish, press Ctrl+C.', :pink)
|
24
|
+
Signal.trap('SIGINT') do
|
25
|
+
puts ''
|
26
|
+
break
|
27
|
+
end
|
28
|
+
loop do
|
29
|
+
record
|
30
|
+
end
|
31
|
+
paint_puts("=" * 80, :green)
|
32
|
+
paint_puts("Input is done.", :green)
|
33
|
+
@words
|
34
|
+
end
|
35
|
+
|
36
|
+
def paint(text, color)
|
37
|
+
case color
|
38
|
+
when :green
|
39
|
+
color_code = 32
|
40
|
+
when :yellow
|
41
|
+
color_code = 33
|
42
|
+
when :blue
|
43
|
+
color_code = 34
|
44
|
+
when :pink
|
45
|
+
color_code = 35
|
46
|
+
else
|
47
|
+
color_code = 0
|
48
|
+
end
|
49
|
+
"\e[#{color_code}m#{text}\e[0m"
|
50
|
+
end
|
51
|
+
|
52
|
+
def paint_print(text, color)
|
53
|
+
print(paint(text, color))
|
54
|
+
end
|
55
|
+
|
56
|
+
def paint_puts(text, color)
|
57
|
+
puts(paint(text, color))
|
58
|
+
end
|
59
|
+
|
60
|
+
private :get_input_if_not_nil, :record, :paint, :paint_print, :paint_puts
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Dictation
|
6
|
+
class Word
|
7
|
+
attr_accessor :value, :translation
|
8
|
+
|
9
|
+
def initialize(value, translation = nil)
|
10
|
+
@value = value
|
11
|
+
@translation = translation
|
12
|
+
end
|
13
|
+
|
14
|
+
def orthography
|
15
|
+
decompose(@value)
|
16
|
+
end
|
17
|
+
|
18
|
+
def decompose(word)
|
19
|
+
word.split(//).map! do |x|
|
20
|
+
normalize(x)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def normalize(letter)
|
25
|
+
letter = letter.downcase.chomp
|
26
|
+
case letter
|
27
|
+
when 'ß'
|
28
|
+
'ss'
|
29
|
+
else
|
30
|
+
letter
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_json(*args)
|
35
|
+
{
|
36
|
+
'json_class' => self.class.name,
|
37
|
+
'data' => [ @value, @translation ]
|
38
|
+
}.to_json(*args)
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def json_create(object)
|
43
|
+
new(*object['data'])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private :decompose, :normalize
|
48
|
+
end
|
49
|
+
end
|
data/lib/dictation.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir[(File.expand_path(File.dirname(__FILE__)) + "/dictation/*.rb")].each { |file| require file }
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dictation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jing Li
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-27 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Practice foreign language by listening and writing (only for Mac OS X).
|
14
|
+
email:
|
15
|
+
- thyrlian@gmail.com
|
16
|
+
executables:
|
17
|
+
- dictation
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/dictation.rb
|
22
|
+
- lib/dictation/operating_system.rb
|
23
|
+
- lib/dictation/word.rb
|
24
|
+
- lib/dictation/serializer.rb
|
25
|
+
- lib/dictation/tts.rb
|
26
|
+
- lib/dictation/teacher.rb
|
27
|
+
- lib/dictation/typewriter.rb
|
28
|
+
- lib/dictation/commander.rb
|
29
|
+
- lib/dictation/control_center.rb
|
30
|
+
- bin/dictation
|
31
|
+
homepage: http://github.com/thyrlian/dictation
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.1.4
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Practice foreign language by listening and writing.
|
55
|
+
test_files: []
|