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.
@@ -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'