amagi-captions 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,122 @@
1
+ module Captions
2
+ class VTT < Base
3
+
4
+ # Header used for all VTT files
5
+ VTT_HEADER = "WEBVTT"
6
+
7
+ # VTT file comments/style section
8
+ VTT_METADATA = /^NOTE|^STYLE/
9
+
10
+ # Auto Keyword used in Alignment
11
+ AUTO_KEYWORD = "auto"
12
+
13
+ # Alignment Data
14
+ ALIGNMENT_VALUES = {
15
+ "middle" => "middle",
16
+ "center" => "middle",
17
+ "left" => "left",
18
+ "right" => "right",
19
+ "start" => "start",
20
+ "end" => "end",
21
+ }
22
+
23
+ # Parse VTT file and update CueList
24
+ def parse
25
+ base_parser do
26
+ count = 1
27
+ cue_count = 0
28
+ meta_data_section = false
29
+ cue = nil
30
+ raise InvalidSubtitle, "Invalid VTT Signature" unless validate_header(@file.gets)
31
+ while(line = @file.gets) do
32
+ line = line.strip
33
+ if line.empty?
34
+ meta_data_section = false
35
+ elsif is_meta_data?(line)
36
+ meta_data_section = true
37
+ elsif is_time?(line)
38
+ @cue_list.append(cue) if cue
39
+ cue_count += 1
40
+ cue = Cue.new
41
+ cue.number = cue_count
42
+ line = line.split
43
+ cue.set_time(line[0], line[2])
44
+ set_properties(cue, line[3..-1])
45
+ elsif !meta_data_section and is_text?(line)
46
+ cue.add_text(line)
47
+ end
48
+ end
49
+ @cue_list.append(cue) if cue
50
+ end
51
+ end
52
+
53
+ # Export CueList to VTT file
54
+ def dump(file)
55
+ base_dump(file) do |file|
56
+ file.write(VTT_HEADER)
57
+ @cue_list.each do |cue|
58
+ file.write("\n\n")
59
+ file.write(msec_to_timecode(cue.start_time))
60
+ file.write(" --> ")
61
+ file.write(msec_to_timecode(cue.end_time))
62
+ file.write("\n")
63
+ file.write(cue.text)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Check whether its a VTT_HEADER or not
69
+ def validate_header(line)
70
+ !!line.strip.match(/^#{VTT_HEADER}/)
71
+ end
72
+
73
+ # Check whether its a meta-data or not
74
+ def is_meta_data?(text)
75
+ !!text.match(VTT_METADATA)
76
+ end
77
+
78
+ # Timecode format used in VTT file
79
+ def is_time?(text)
80
+ !!text.match(/^(\d{2}:)?\d{2}:\d{2}.\d{3}.*(\d{2}:)?\d{2}:\d{2}.\d{3}/)
81
+ end
82
+
83
+ # Check whether if its subtilte text or not
84
+ def is_text?(text)
85
+ !text.empty? and text.is_a?(String) and text != VTT_HEADER
86
+ end
87
+
88
+ def set_properties(cue, properties)
89
+ properties.each do |prop|
90
+ prop, value = prop.split(":")
91
+ value.gsub!("%","")
92
+ case prop
93
+ when "align"
94
+ cue.alignment = get_alignment(value)
95
+ when "line"
96
+ value = value.split(",")[0]
97
+ cue.position = get_line(value)
98
+ end
99
+ end
100
+ end
101
+
102
+ def get_alignment(value)
103
+ raise InvalidSubtitle, "Invalid VTT Alignment Property" unless ALIGNMENT_VALUES[value]
104
+ return ALIGNMENT_VALUES[value]
105
+ end
106
+
107
+ def get_line(value)
108
+ raise InvalidSubtitle, "VTT Line property should be a valid number" if !is_integer?(value) and value != AUTO_KEYWORD
109
+ return value.to_i
110
+ end
111
+
112
+ def get_position(value)
113
+ raise InvalidSubtitle, "VTT Position should be a valid number" if !is_integer?(value)
114
+ raise InvalidSubtitle, "VTT Position should be a number between 0 to 100" if (value.to_i < 0) or (value.to_i > 100)
115
+ return value.to_i
116
+ end
117
+
118
+ def is_integer?(val)
119
+ val.to_i.to_s == val
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,53 @@
1
+ module Captions
2
+ class CueList
3
+ include Enumerable
4
+
5
+ # Creates a new CueList
6
+ def initialize(frame_rate, list=[])
7
+ @fps = frame_rate
8
+ @list = list
9
+ end
10
+
11
+ # Returns frame-rate of the list
12
+ def fps
13
+ @fps
14
+ end
15
+
16
+ # Returns the parsed subtitles
17
+ def entries
18
+ @list
19
+ end
20
+
21
+ # Hide all cues when inspecting CueList
22
+ # Show only necessary info rather than printing everything
23
+ def inspect
24
+ "#<Captions::CueList:#{object_id} @fps=#{fps} @cues=#{@list.count}>"
25
+ end
26
+
27
+ # Changes the frame rate of CueList
28
+ # This also changes frame rate in already parsed
29
+ # subtitles
30
+ def frame_rate=(rate)
31
+ @list.each { |c| c.change_frame_rate(@fps, rate) }
32
+ @fps = rate
33
+ end
34
+
35
+ # Inserts the subtitle into the CueList
36
+ # Subtitle is serialized before its inserted
37
+ def append(cue)
38
+ cue.serialize(@fps)
39
+ @list << cue
40
+ end
41
+
42
+ # Iterate through CueList
43
+ def each
44
+ return to_enum(:each) unless block_given?
45
+ @list.each { |c| yield(c) }
46
+ end
47
+
48
+ # Array based enumerables for cuelist
49
+ def [](index)
50
+ @list[index]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,87 @@
1
+ module Captions
2
+ module Util
3
+
4
+ # TC should be HH:MM:SS:FF (frames) or HH:MM:SS.MSC (milliseconds).
5
+ # FF(2 digits) and MSC(3 digits) are optional.
6
+ TIMECODE_REGEX = /^-?([01]\d|2[0-3]):[0-5]\d:[0-5]\d(:\d{2}|\.\d{3})?$/
7
+
8
+ # Currently considering frame rate as 25
9
+ # Converts time-code in HH:MM:SS.MSEC (or) HH:MM:SS:FF (or) MM:SS.MSEC
10
+ # to milliseconds.
11
+ def convert_to_msec(tc, ms_per_frame=40)
12
+ msec = 0
13
+ negative_multiplier = 1
14
+ if tc[0] == '-'
15
+ tc = tc[1..-1] # remove -ve sign
16
+ negative_multiplier = -1
17
+ end
18
+ tc_split = tc.split('.')
19
+ time_split = tc_split[0].split(':')
20
+
21
+ # To handle MM:SS.MSEC format
22
+ if time_split.length == 2
23
+ time_split.unshift('00')
24
+ end
25
+
26
+ if tc_split[1] # msec component exists
27
+ msec = tc_split[1].ljust(3, '0').to_i # pad with trailing 0s to make it 3 digit
28
+ elsif time_split.length == 4 # FF (frame) component exists
29
+ msec = time_split[-1].to_i * ms_per_frame.to_f
30
+ time_split.pop # so that below code can work from last index
31
+ end
32
+
33
+ min = 60
34
+ hour = min * 60
35
+ # Get HH:MM:SS in seconds
36
+ sec = time_split[-1].to_i
37
+ sec += time_split[-2].to_i * min
38
+ sec += time_split[-3].to_i * hour
39
+
40
+ msec += sec * 1000
41
+
42
+ return (negative_multiplier * msec.round) # to be consistent with tc_to_frames which also rounds
43
+ end
44
+
45
+ # Converts milliseconds calculated in one frame-rate to another frame-rate
46
+ def convert_frame_rate(msec, old_fps, new_fps)
47
+ old_ms_per_frame = (1000.0 / old_fps)
48
+ new_ms_per_frame = (1000.0 / new_fps)
49
+ frames = (msec / old_ms_per_frame).round # Number of frames in old fps
50
+ sec = frames / old_fps
51
+ frames = frames % old_fps
52
+ new_frames = sec * new_fps
53
+ new_frames += frames # Number of frames in new fps
54
+ return (new_frames * new_ms_per_frame).round # MSEC in new fps
55
+ end
56
+
57
+ # Converts milliseconds to timecode format
58
+ # Currently returns HH:MM:SS.MSEC
59
+ # Supports upto 60 hours
60
+ def msec_to_timecode(milliseconds)
61
+ seconds = milliseconds / 1000
62
+ msec = milliseconds % 1000
63
+ secs = seconds % 60
64
+
65
+ seconds = seconds / 60
66
+ mins = seconds % 60
67
+
68
+ seconds = seconds / 60
69
+ hours = seconds % 60
70
+
71
+ format("%02d:%02d:%02d.%03d",hours, mins, secs ,msec)
72
+ end
73
+
74
+ # Parses time-code and converts it to milliseconds.
75
+ # If time cannot be converted to milliseconds,
76
+ # it throws InvalidInput Error
77
+ def sanitize(time, frame_rate)
78
+ if time.is_a?(String)
79
+ if TIMECODE_REGEX.match(time)
80
+ time = convert_to_msec(time, frame_rate)
81
+ end
82
+ end
83
+ raise InvalidInput, 'Input should be in Milliseconds or Timecode' unless time.is_a? (Fixnum)
84
+ return time
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module Captions
2
+ VERSION = "1.3.2"
3
+ end
data/lib/captions.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'captions/version'
2
+ require 'captions/error'
3
+ require 'captions/util'
4
+ require 'captions/base'
5
+ require 'captions/list'
6
+ require 'captions/cue'
7
+ require 'captions/formats/srt'
8
+ require 'captions/formats/vtt'
9
+
10
+ require 'captions/export'