amagi-captions 1.3.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6e6e70d742c9b9f90fb4f494641353e883d9f854a15e50d20e8cc1a0e85295dd
4
+ data.tar.gz: 6fde8355de7aa80ffc716923fac935ae8338a66beb42df0a76b5a4c09f6afbe6
5
+ SHA512:
6
+ metadata.gz: 9a6c0bfc0c3466f0ae922cc46144018e552a7cbd2fddc6c35de807368951eef6fc8a3bb3f2166852dcb8fa26063bc56bf29f87629f28423e4bc1404d2fca7789
7
+ data.tar.gz: 3001b3ce3620f766a2b7698b403747d290c604323119daeb32086eef80453b4104db59ab9ae56fd793f4ad95850f5f41c83652709837910985a3774200dec2b9
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ cache: bundler
3
+
4
+ rvm:
5
+ - ruby-2.3.8
6
+
7
+ script: 'bundle exec rspec'
8
+
9
+ notifications:
10
+ email:
11
+ recipients:
12
+ - navinre93@gmail.com
13
+ on_failure: change
14
+ on_success: never
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in captions.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 navin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Captions - Subtitle Editor and Converter
2
+
3
+ [![Build Status](https://travis-ci.org/navinre/captions.svg?branch=master)](https://travis-ci.org/navinre/captions)
4
+
5
+ Captions can read subtitles of various formats, modify them and convert the subtitle to other formats. Currently captions supports `SRT`, `WebVTT` files.
6
+
7
+ ## Supported Features
8
+ - Read subtitles
9
+ - Move subtitles
10
+ - Increase duration
11
+ - Filter subtitles
12
+ - Change frame rate
13
+ - Convert to other format (SRT, WebVTT)
14
+
15
+ ## Features Planned
16
+ - Read subtitle properties (Font Style, Aligment, Position etc.)
17
+ - Delete subtitles
18
+ - Change subtitle fonts
19
+ - Command Line Interface
20
+ - Spell Check
21
+ - And More...
22
+
23
+
24
+ ## Usage
25
+
26
+ **Read subtitle file:**
27
+
28
+ ```ruby
29
+ s = Captions::SRT.new('test.srt')
30
+ s.parse
31
+
32
+ s = Captions::VTT.new('test.vtt')
33
+ s.parse
34
+ ```
35
+
36
+ **Filter subtitles:**
37
+
38
+ Once the subtitle file has been parsed. Every subtitle will get **start_time, end_time, duration and text**. All the values are stored in milliseconds. `Captions::InvalidSubtitle` error will be thown if start-time or end-time cannot be found for a subtitle.
39
+
40
+ View all subtitle text:
41
+ ```ruby
42
+ s.cues.map { |c| c.text }
43
+ ```
44
+
45
+ Filter subtitles based on condition:
46
+ ```ruby
47
+ s.cues { |c| c.start_time > 1000 }
48
+ s.cues { |c| c.end_time > 1000 }
49
+ s.cues { |c| c.duration > 1000 }
50
+ ```
51
+
52
+ To get all subtitles:
53
+ ```ruby
54
+ s.cues.each { |c| puts c }
55
+ ```
56
+
57
+ **Move Subtitle:**
58
+
59
+ ```ruby
60
+ s.move_by(1000)
61
+ s.move_by(1000) { |c| c.start_time > 3000 }
62
+ ```
63
+
64
+ Former command moves all subtitles by 1 second. Later moves the subtitles which are starting after 3 seconds by 1 second.
65
+
66
+ **Increase Duration:**
67
+
68
+ ```ruby
69
+ s.increase_duration_by(1000)
70
+ s.increase_duration_by(1000) { |c| c.start_time > 3000 }
71
+ ```
72
+
73
+ Former command increases duration of all subtitles by 1 second. Later increases the duration of subtitles which are starting after 3 seconds by 1 second.
74
+
75
+ **Change Frame Rate:**
76
+
77
+ All subtitles are parsed based on **25 frames/second** by default. This can be changed by passing frame rate at the time of reading the subtitle file.
78
+
79
+ ```ruby
80
+ s = Captions::SRT.new('test.srt', 29.97)
81
+ s.parse
82
+ ```
83
+
84
+ Frame rate can also be changed after parsing. This command changes all the subtitles which are parsed in different frame-rate to new frame-rate.
85
+
86
+ ```ruby
87
+ s.set_frame_rate(29.97)
88
+ ```
89
+
90
+ **Convert to Other Format:**
91
+
92
+ Subtitles parsed in one format can be converted to other format. Currently export is supported for **SRT** and **WebVTT**. Other formats will be added soon.
93
+
94
+ ```ruby
95
+ s = Captions::SRT.new('test.srt')
96
+ s.parse
97
+ s.export_to_vtt('test.vtt')
98
+ ```
99
+
100
+ Subtitles can also be filtered and those filtered subtitles can be written to a file.
101
+
102
+ ```ruby
103
+ s = Captions::SRT.new('test.srt')
104
+ s.parse
105
+ new_cues = s.cues { |c| c.start_time > 2000 }
106
+ s.export_to_vtt('test.vtt', new_cues)
107
+ ```
108
+
109
+ **Filter subtiltes and Export:**
110
+
111
+ Subtitles can be filtered with filter option. This returns a new `Captions` object. Once filters new object is created and cues for that becomes the result of the filter. This can be exported to other formats easily.
112
+
113
+ ```ruby
114
+ new_obj = s.filter { |c| c.start_time > 2000 }
115
+ new_obj.export_to_vtt('test.vtt')
116
+ ```
117
+
118
+ ## Installation
119
+
120
+ Add this line to your application's Gemfile:
121
+
122
+ ```ruby
123
+ gem 'captions'
124
+ ```
125
+
126
+ Or install it yourself as:
127
+
128
+ $ gem install captions
129
+
130
+
131
+ ## Development
132
+
133
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
134
+
135
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
136
+
137
+ ## Contributing
138
+
139
+ Bug reports and pull requests are welcome on GitHub at https://github.com/navinre/captions.
140
+
141
+
142
+ ## License
143
+
144
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
145
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'captions/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "amagi-captions"
8
+ spec.version = Captions::VERSION
9
+ spec.authors = ["navin"]
10
+ spec.email = ["cloudport.team@amagi.com"]
11
+
12
+ spec.summary = %q{Subtitle Editor and Converter written in Ruby}
13
+ spec.description = %q{Subtitle Editor and Converter written in Ruby. Captions can read/modify/export subtitles from one format to another }
14
+ spec.homepage = "https://github.com/amagimedia/captions"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "captions"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,179 @@
1
+ module Captions
2
+ class Base
3
+
4
+ # Adding Util methods so that it can be used by
5
+ # any subtitle parser
6
+ include Util
7
+
8
+ # Creates new instance of parser
9
+ # Usage:
10
+ # p = Captions::Base.new(nil, 25)
11
+ #
12
+ # This creates new cue-list with no file object
13
+ # associated with it. `parse` method cannot be
14
+ # called if no file path is specified.
15
+ #
16
+ # p = Captions::Base.new('file_path', 25)
17
+ #
18
+ # This parses the file specified in the file path
19
+ # `parse` method will be defined in any one of the
20
+ # classes which extends this base class. This parses
21
+ # the file in the fps specified. If a subtitle file
22
+ # has to be parsed in different fps, it can be passed
23
+ # as a parameter. FPS parameter plays an important
24
+ # role if the start-time or end-time of a subtitle is
25
+ # mentioned in frames (i.e) HH:MM:SS:FF (frames)
26
+ def initialize(file=nil, fps=25)
27
+ @cue_list = CueList.new(fps)
28
+ @file = File.new(file, 'r:bom|utf-8') if file
29
+ end
30
+
31
+ # This overrides the existing cuelist which has been
32
+ # populated with a new cuelist. This is mainly used in
33
+ # export of one format to another.
34
+ def cues=(cue_list)
35
+ @cue_list = CueList.new(@cue_list.fps, cue_list)
36
+ end
37
+
38
+ ############## BEGINNING OF OPERATIONS #################
39
+ ## Following Operations can performed on subtitles
40
+
41
+ # A subtitle is parsed with 25 fps by default. This default
42
+ # value can be changed when creating a new parser class.
43
+ # When the subtitle is being parsed, it takes the value
44
+ # mentioned when the class is created. Even after the
45
+ # subtitle is parsed, frame rate (fps) can be changed using
46
+ # this method.
47
+ # Usage:
48
+ # p = Captions::Base.new('file_path', 25)
49
+ # p.parse
50
+ # p.set_frame_rate(29.97)
51
+ #
52
+ # This method changes all the subtitle which are parsed to
53
+ # the new frame rate.
54
+ def set_frame_rate(rate)
55
+ @cue_list.frame_rate = rate
56
+ end
57
+
58
+ # This returns the subtitle which are parsed. A block
59
+ # can also be passed to filter the cues based on following
60
+ # parameters.
61
+ # Usage:
62
+ # p.cues
63
+ # p.cues { |c| c.start_time > 1000 }
64
+ # p.cues { |c| c.end_time > 1000 }
65
+ # p.cues { |c| c.duration > 1000 }
66
+ #
67
+ # Filters based on the condition and returns new set of cues
68
+ def cues(&block)
69
+ if block_given?
70
+ base = self.class.new()
71
+ base.cues = fetch_result(&block)
72
+ return base.cues
73
+ else
74
+ @cue_list
75
+ end
76
+ end
77
+
78
+ # This filters the subtitle based on the condition and returns
79
+ # a new object. A condition is mandatory to filter subtitles
80
+ # Usage:
81
+ # p.filter
82
+ # p.filter { |c| c.start_time > 1000 }
83
+ # p.filter { |c| c.end_time > 1000 }
84
+ # p.filter { |c| c.duration > 1000 }
85
+ #
86
+ # Filters and returns a Captions class rather than a cue list
87
+ def filter(&block)
88
+ if block_given?
89
+ base = self.class.new()
90
+ base.cues = fetch_result(&block)
91
+ return base
92
+ end
93
+ end
94
+
95
+ # Moves subtitles by `n` milliseconds
96
+ # Usage:
97
+ # p.move_by(1000)
98
+ # p.move_by("00:00:02.000")
99
+ # p.move_by(1000) { |c| c.start_time > 2000 }
100
+ #
101
+ # This changes start-time and end-time of subtiltes by
102
+ # the time passed.
103
+ def move_by(diff, &block)
104
+ msec = sanitize(diff, frame_rate)
105
+ fetch_result(&block).each do |cue|
106
+ cue.start_time += msec
107
+ cue.end_time += msec
108
+ end
109
+ end
110
+
111
+ # Increases duration of subtitles by `n` milliseconds
112
+ # Usage:
113
+ # p.increase_duration_by(1000)
114
+ # p.increase_duration_by("00:00:02.000")
115
+ # p.increase_duration_by(1000) { |c| c.start_time > 2000 }
116
+ #
117
+ # This increases duration of subtiltes by the time passed.
118
+ def increase_duration_by(diff, &block)
119
+ msec = sanitize(diff, frame_rate)
120
+ fetch_result(&block).each do |cue|
121
+ cue.duration += msec
122
+ end
123
+ end
124
+
125
+ ################ END OF OPERATIONS ###################
126
+
127
+ private
128
+
129
+ # This is the base method through which all subtitle
130
+ # file parsing should be done. This throws error if
131
+ # no `@file` is found. This also closes the file once
132
+ # the file has be parsed. All other logic like when the
133
+ # subtitle has to be inserted into the list, when to set
134
+ # the start and end time will be defined inside `parse`
135
+ # method.
136
+ def base_parser
137
+ raise UnknownFile, "No subtitle file specified" if @file.nil?
138
+ begin
139
+ yield
140
+ ensure
141
+ @file.close
142
+ end
143
+ return true
144
+ end
145
+
146
+ # This is the base method through which all subtitle
147
+ # exports has to be done. When a `file_path` is passed
148
+ # to this base method, it opens the file. Other logic
149
+ # about how the file should be written is defined inside
150
+ # `dump` method in one of the subclass.
151
+ def base_dump(file)
152
+ begin
153
+ File.open(file, 'w') do |file|
154
+ yield(file)
155
+ end
156
+ end
157
+ return true
158
+ end
159
+
160
+ # This returns the frame-rate which was used for parsing
161
+ # the subtitles
162
+ def frame_rate
163
+ @cue_list.fps
164
+ end
165
+
166
+ # This accepts a block and returns the result based
167
+ # on the condition passed. This acts on the cuelist
168
+ # whenever a block is passed it does select with the
169
+ # condition passed and returns the result. When no
170
+ # block is passed, it returns the entire cuelist.
171
+ def fetch_result(&block)
172
+ if block_given?
173
+ return @cue_list.select(&block)
174
+ else
175
+ return @cue_list
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,93 @@
1
+ module Captions
2
+ class Cue
3
+ include Util
4
+
5
+ # Text Properties supported
6
+ ALIGNMENT = "alignment"
7
+ COLOR = "color"
8
+ POSITION = "position"
9
+
10
+ # List of Text Properties
11
+ TEXT_PROPERTIES = [ALIGNMENT, COLOR, POSITION]
12
+
13
+ attr_accessor :number, :start_time, :end_time, :duration, :text, :properties
14
+
15
+ # Creates a new Cue class
16
+ # Each cue denotes a subtitle.
17
+ def initialize(cue_number = nil)
18
+ self.text = nil
19
+ self.start_time = nil
20
+ self.end_time = nil
21
+ self.duration = nil
22
+ self.number = cue_number
23
+ self.properties = {}
24
+ end
25
+
26
+ # Sets the time for the cue. Both start-time and
27
+ # end-time can be passed together. This just assigns
28
+ # the value passed.
29
+ def set_time(start_time, end_time, duration = nil)
30
+ self.start_time = start_time
31
+ self.end_time = end_time
32
+ self.duration = duration
33
+ end
34
+
35
+ # Getter and Setter methods for Text Properties
36
+ # These are pre-defined properties. This is just to assign
37
+ # or access the properties of that text.
38
+ TEXT_PROPERTIES.each do |setting|
39
+ define_method :"#{setting}" do
40
+ if self.properties[setting].present?
41
+ return self.properties[setting]
42
+ end
43
+ return nil
44
+ end
45
+
46
+ define_method :"#{setting}=" do |value|
47
+ self.properties[setting] = value
48
+ end
49
+ end
50
+
51
+ # Serializes the values set for the cue.
52
+ # Converts start-time, end-time and duration to milliseconds
53
+ # If duration is not found, it will be calculated based on
54
+ # start-time and end-time.
55
+ def serialize(fps)
56
+ raise InvalidSubtitle, "Subtitle should have start time" if self.start_time.nil?
57
+ raise InvalidSubtitle, "Subtitle shold have end time" if self.end_time.nil?
58
+
59
+ begin
60
+ ms_per_frame = (1000.0 / fps)
61
+ self.start_time = convert_to_msec(self.start_time, ms_per_frame)
62
+ self.end_time = convert_to_msec(self.end_time, ms_per_frame)
63
+ if duration.nil?
64
+ self.duration = self.end_time - self.start_time
65
+ else
66
+ self.duration = convert_to_msec(self.duration, ms_per_frame)
67
+ end
68
+ rescue
69
+ raise InvalidSubtitle, "Cannot calculate start-time or end-time"
70
+ end
71
+ end
72
+
73
+ # Changes start-time, end-time and duration based on new frame-rate
74
+ def change_frame_rate(old_rate, new_rate)
75
+ self.start_time = convert_frame_rate(self.start_time, old_rate, new_rate)
76
+ self.end_time = convert_frame_rate(self.end_time, old_rate, new_rate)
77
+ self.duration = convert_frame_rate(self.duration, old_rate, new_rate)
78
+ end
79
+
80
+ # Adds text. If text is already found, new-line is appended.
81
+ def add_text(text)
82
+ if self.text.nil?
83
+ self.text = text
84
+ else
85
+ self.text += "\n" + text
86
+ end
87
+ end
88
+
89
+ def <=>(other_cue)
90
+ self.start_time <=> other_cue.start_time
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ module Captions
2
+
3
+ # Used for all execptions thrown from captions
4
+ class CaptionsError < StandardError
5
+ end
6
+
7
+ # Subtitle has to be in correct format
8
+ # when its being serialized
9
+ class InvalidSubtitle < CaptionsError
10
+ end
11
+
12
+ # Subtitle has to be correct format
13
+ # when its being parsed
14
+ class MalformedString < CaptionsError
15
+ end
16
+
17
+ # File has to be specifed for parsing
18
+ # subtitle
19
+ class UnknownFile < CaptionsError
20
+ end
21
+
22
+ # Input has to be proper for filtering
23
+ # subtitles
24
+ class InvalidInput < CaptionsError
25
+ end
26
+
27
+ end
@@ -0,0 +1,25 @@
1
+ module Captions
2
+
3
+ # This module adds support for export of subtitles
4
+ # from one format to another. Subclasses which
5
+ # extends Captions::Base needs to `dump` method to
6
+ # support export to that corresponding format
7
+ module Export
8
+ Captions.constants.each do |format|
9
+ obj = Captions.const_get(format)
10
+ next unless obj.is_a?(Class) and (obj.superclass == Captions::Base)
11
+ method_name = "export_to_" + format.to_s.downcase
12
+ define_method(method_name) do |file, cues=nil|
13
+ sub_format = obj.new()
14
+ sub_format.cues = cues || self.cues.dup
15
+ sub_format.dump(file) if sub_format.respond_to?(:dump)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Including this module after all the sub-classes of
21
+ # Captions::Base has been defined.
22
+ Captions::Base.send(:include, Export)
23
+
24
+ end
25
+
@@ -0,0 +1,83 @@
1
+ module Captions
2
+ class SRT < Base
3
+
4
+ def parse
5
+ base_parser do
6
+ count = 0
7
+ state = :new_cue
8
+ cue = nil
9
+ loop do
10
+ count += 1
11
+ line = @file.gets
12
+ break if line.nil? ## End of file
13
+ line.chomp!
14
+ case state
15
+ when :new_cue
16
+ line.strip!
17
+ next if line.empty? ## just another blank line, remain in new_cue state
18
+ begin
19
+ cue = Cue.new(Integer(line))
20
+ rescue ArgumentError
21
+ raise InvalidSubtitle, "Invalid Cue Number at line #{count}"
22
+ end
23
+ state = :time
24
+ when :time
25
+ line.strip!
26
+ raise InvalidSubtitle, "Invalid Time Format at line #{count}" unless is_time?(line)
27
+ start_time, end_time = get_time(line)
28
+ cue.set_time(start_time, end_time)
29
+ state = :text
30
+ when :text
31
+ if line.empty?
32
+ ## end of previous cue
33
+ @cue_list.append(cue) if cue && cue.start_time
34
+ cue = nil
35
+ state = :new_cue
36
+ else
37
+ line.strip!
38
+ cue.add_text(line)
39
+ end
40
+ end
41
+ end
42
+ @cue_list.append(cue) if cue && cue.start_time
43
+ end
44
+ end
45
+
46
+ def dump(file)
47
+ base_dump(file) do |file|
48
+ @cue_list.each do |cue|
49
+ file.write(cue.number)
50
+ file.write("\n")
51
+ file.write(msec_to_timecode(cue.start_time).gsub!('.' , ','))
52
+ file.write(" --> ")
53
+ file.write(msec_to_timecode(cue.end_time).gsub!('.' , ','))
54
+ file.write("\n")
55
+ file.write(cue.text)
56
+ file.write("\n\n")
57
+ end
58
+ end
59
+ end
60
+
61
+ def get_time(line)
62
+ data = line.split('-->')
63
+ return format_time(data[0]), format_time(data[1])
64
+ end
65
+
66
+ def format_time(text)
67
+ text.strip.gsub(/,/,".")
68
+ end
69
+
70
+ def is_number?(text)
71
+ !!text.match(/^\d+$/)
72
+ end
73
+
74
+ def is_time?(text)
75
+ !!text.match(/^\d{2}:\d{2}:\d{2},\d{3}.*\d{2}:\d{2}:\d{2},\d{3}$/)
76
+ end
77
+
78
+ def is_text?(text)
79
+ !text.empty? and text.is_a?(String)
80
+ end
81
+
82
+ end
83
+ end