dictation 1.0.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 +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: []
|