v2av 0.1.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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +1 -0
  3. data/lib/v2av.rb +315 -0
  4. data.tar.gz.sig +0 -0
  5. metadata +170 -0
  6. metadata.gz.sig +2 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e7da8afea58a1871295561eee8f90e398bec6eb98ff1a8d2ba2a30bc997d35b5
4
+ data.tar.gz: 921dafddb47f35ac522ecb56f3143c8a8945a1b363d4f3a883e4ff8777e51726
5
+ SHA512:
6
+ metadata.gz: 3918d1b9de5d199346e3453936ddb4a19647963aebadddb0c033407cecfe9894035e0a42da43ae4d47f848f44d62f4f7858fbc6d809ffac3e5ff42ba98c58895
7
+ data.tar.gz: 9a70a295daf690f261f81a801154d5604458e61c72bbc7c76103277966b2d9f9615f33c6b5b35216cdc1209663abe0070b9b7460dde616559f4f19652147f0ab
checksums.yaml.gz.sig ADDED
@@ -0,0 +1 @@
1
+ -OQkhC!g8��੅^�g-�B���,�7�X���Ob��u�}ػ��o�ŧT���ȗ��B3$-"���ϛ7EB�f��p�n�����){��2t ���%�1��"�N�؄1�Z
data/lib/v2av.rb ADDED
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # file: v2av.rb
4
+
5
+ require 'ostruct'
6
+ require 'subunit'
7
+ require 'zip/zip'
8
+ require 'ogginfo'
9
+ require 'wavefile'
10
+ require 'rxfhelper'
11
+ require 'pollyspeech'
12
+ require 'archive/zip'
13
+
14
+
15
+ module WavTool
16
+ include WaveFile
17
+
18
+ def wav_silence(filename, duration: 1)
19
+
20
+ square_cycle = [0] * 100 * duration
21
+ buffer = Buffer.new(square_cycle, Format.new(:mono, :float, 44100))
22
+
23
+ Writer.new(filename, Format.new(:mono, :pcm_16, 22050)) do |writer|
24
+ 220.times { writer.write(buffer) }
25
+ end
26
+
27
+ end
28
+
29
+ def wav_concat(files, save_file='audio.wav')
30
+
31
+ Writer.new(save_file, Format.new(:stereo, :pcm_16, 22050)) do |writer|
32
+
33
+ files.each do |file_name|
34
+
35
+ Reader.new(file_name).each_buffer(samples_per_buffer=4096) do |buffer|
36
+ writer.write(buffer)
37
+ end
38
+
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ def ogg_to_wav(oggfile, wavfile=oggfile.sub(/\.ogg$/,'.wav'))
45
+
46
+ if block_given? then
47
+ yield(oggfile)
48
+ else
49
+ `oggdec #{oggfile}`
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ module TimeHelper
57
+
58
+ refine String do
59
+ def to_time()
60
+ Time.strptime(self, "%H:%M:%S")
61
+ end
62
+ end
63
+
64
+ refine Integer do
65
+ def to_hms
66
+ Subunit.new(units={minutes:60}, seconds: self).to_a
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ class V2av
73
+ using ColouredText
74
+ using TimeHelper
75
+ include WavTool
76
+
77
+ def initialize(src, srt, working_dir: '/tmp/v2av', debug: false,
78
+ pollyspeech: {access_key: nil, secret_key: nil, voice_id: 'Amy',
79
+ cache_filepath: '/tmp/v2av/pollyspeech/cache'})
80
+
81
+ @working_dir, @debug = working_dir, debug
82
+
83
+ @steps = srt.lines.map do |x|
84
+ raw_time, desc = rx.split(/ +/,)
85
+ OpenStruct.new({time: raw_time.to_i, desc: desc})
86
+ end
87
+
88
+ @pollyspeech = PollySpeech.new(pollyspeech) if pollyspeech[:access_key]
89
+ end
90
+
91
+ def build()
92
+
93
+ if block_given? then
94
+
95
+ yield(self)
96
+
97
+ else
98
+
99
+ dir = File.dirname(source)
100
+ file = File.basename(source)
101
+
102
+ tidy!
103
+
104
+ vid2 = File.join(dir, file.sub(/\.mp4$/,'b\0'))
105
+ trim_video source, vid2
106
+
107
+ vid3 = File.join(dir, file.sub(/\.mp4$/,'c.avi'))
108
+
109
+ generate_audio
110
+ add_audio_track File.join(@working_dir, 'audio.wav'), vid2, vid3
111
+
112
+
113
+ vid4 = File.join(dir, file.sub(/\.avi$/,'d\0'))
114
+ resize_video vid3, vid4
115
+
116
+ vid5 = File.join(dir, file.sub(/\.mp4$/,'e\0'))
117
+ transcode_video(vid4, vid5)
118
+ add_subtitles(vid5, destination)
119
+
120
+ end
121
+
122
+ end
123
+
124
+ private
125
+
126
+ # adds the audio track to the video file
127
+ # mp4 in avi out
128
+ #
129
+ def add_audio_track(audio_file, video_file, target_video)
130
+
131
+ if block_given? then
132
+ yield(audio_file, video_file, target_video)
133
+ else
134
+ `ffmpeg -i #{video_file} -i #{audio_file} -codec copy -shortest #{target_video} -y`
135
+ end
136
+
137
+ end
138
+
139
+ # mp4 in mp4 out
140
+ #
141
+ def add_subtitles(source, destination)
142
+
143
+
144
+ subtitles = File.join(@working_dir, 's.srt')
145
+ File.write subtitles, to_srt()
146
+
147
+ if block_given? then
148
+ yield(source, subtitles, destination)
149
+ else
150
+ `ffmpeg -i #{source} -i #{subtitles} -c copy -c:s mov_text #{destination} -y`
151
+ end
152
+
153
+ end
154
+
155
+ def generate_audio(wav: true)
156
+
157
+ return nil unless @pollyspeech
158
+
159
+ @steps.each.with_index do |x, i|
160
+
161
+ puts 'x.desc: ' + x.desc.inspect if @debug
162
+ filename = "voice#{i+1}.ogg"
163
+
164
+ x.audio = filename
165
+ file = File.join(@working_dir, filename)
166
+ @pollyspeech.tts(x.desc.force_encoding('UTF-8'), file)
167
+
168
+ x.audio_duration = OggInfo.open(file) {|ogg| ogg.length.to_i }
169
+
170
+ if @debug then
171
+ puts ('x.duration: ' + x.duration.inspect).debug
172
+ puts ('x.audio_duration: ' + x.audio_duration.inspect).debug
173
+ end
174
+
175
+ duration = x.duration - x.audio_duration
176
+ x.silence_duration = duration >= 0 ? duration : 0
177
+
178
+ if wav then
179
+
180
+ silent_file = File.join(@working_dir, "silence#{(i+1).to_s}.wav")
181
+ puts 'x.silence_duration: ' + x.silence_duration.inspect if @debug
182
+ wav_silence silent_file, duration: x.silence_duration
183
+ ogg_to_wav File.join(@working_dir, "voice#{i+1}.ogg")
184
+
185
+ end
186
+
187
+ sleep 0.02
188
+
189
+ end
190
+
191
+ if wav then
192
+
193
+ intro = File.join(@working_dir, 'intro.wav')
194
+ wav_silence intro
195
+
196
+ files = @steps.length.times.flat_map do |n|
197
+ [
198
+ File.join(@working_dir, "voice#{n+1}.wav"),
199
+ File.join(@working_dir, "silence#{n+1}.wav")
200
+ ]
201
+ end
202
+
203
+ files.prepend intro
204
+
205
+ wav_concat files, File.join(@working_dir, 'audio.wav')
206
+ end
207
+
208
+ end
209
+
210
+ # avi in avi out
211
+ def resize_video(source, destination)
212
+ `ffmpeg -i #{source} -vf scale="720:-1" #{destination} -y`
213
+ end
214
+
215
+ def tidy!()
216
+
217
+ verbose_level = 0
218
+
219
+ @steps.each do |x|
220
+
221
+ x.desc.gsub!(/\s*\([^\)]+\)\s*/,'')
222
+ x.desc.sub!(/ in "\w+"$/,'')
223
+ x.desc.sub!(/"User account for [^"]+"/,'the User account icon.')
224
+
225
+ if x.desc =~ /User left click/ and verbose_level == 0 then
226
+
227
+ x.desc.sub!(/User left click/, 'Using the mouse, left click')
228
+ verbose_level = 1
229
+
230
+ elsif x.desc =~ /User left click/ and verbose_level == 1
231
+
232
+ x.desc.sub!(/User left click/, 'Left click')
233
+ verbose_level = 2
234
+
235
+ elsif x.desc =~ /User left click/ and verbose_level == 2
236
+
237
+ x.desc.sub!(/User left click/, 'Click')
238
+ verbose_level = 3
239
+
240
+ elsif x.desc =~ /User left click/ and verbose_level == 3
241
+
242
+ x.desc.sub!(/User left click on/, 'Select')
243
+
244
+ else
245
+ verbose_level = 0
246
+ end
247
+
248
+ end
249
+
250
+ end
251
+
252
+ def to_srt(offset=-(@steps.first.time - 1))
253
+
254
+ lines = to_subtitles(offset).strip.lines.map.with_index do |x, i|
255
+
256
+ raw_times, subtitle = x.split(/ /,2)
257
+ puts ('raw_times: ' + raw_times.inspect).debug if @debug
258
+ start_time, end_time = raw_times.split('-',2)
259
+ times = [("%02d:%02d:%02d,000" % ([0, 0 ] + start_time.split(/\D/)\
260
+ .map(&:to_i)).reverse.take(3).reverse), \
261
+ '-->', \
262
+ ("%02d:%02d:%02d,000" % ([0, 0 ] + end_time.split(/\D/).map(&:to_i))\
263
+ .reverse.take(3).reverse)].join(' ')
264
+
265
+ [i+1, times, subtitle].join("\n")
266
+
267
+ end
268
+
269
+ lines.join("\n")
270
+
271
+ end
272
+
273
+ def to_subtitles(offset=-(@steps.first.time - 1))
274
+
275
+ raw_times = @steps.map {|x| [x.time, x.time + x.audio_duration + 1]}
276
+
277
+
278
+ times = raw_times.map do |x|
279
+
280
+ x.map do |sec|
281
+ a = Subunit.new(units={minutes:60}, seconds: sec+offset).to_h.to_a
282
+ a.map {|x|"%d%s" % [x[1], x[0][0]] }.join('')
283
+ end.join('-')
284
+
285
+ end
286
+
287
+ times.zip(@steps.map(&:desc)).map {|x| x.join(' ')}.join("\n")
288
+
289
+ end
290
+
291
+ def transcode_video(avi, mp4)
292
+
293
+ if block_given? then
294
+ yield(avi, mp4)
295
+ else
296
+ `ffmpeg -i #{avi} #{mp4} -y`
297
+ end
298
+
299
+ end
300
+
301
+ def trim_video(video, newvideo)
302
+
303
+ start = @steps.first.time - 4
304
+ t1, t2 = [start, @steps.last.time - 2 ].map do |step|
305
+ "%02d:%02d:%02d" % (step.to_hms.reverse + [0,0]).take(3).reverse
306
+ end
307
+
308
+ `ffmpeg -i #{video} -ss #{t1} -t #{t2} -async 1 #{newvideo} -y`
309
+
310
+ end
311
+
312
+ end
313
+
314
+ v2 = V2av.new('video.mp4', 's.srt', working_dir: '/tmp/v2av')
315
+ v2.build('video2.mp4')
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: v2av
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Robertson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIEXjCCAsagAwIBAgIBATANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQDDCFnZW1t
14
+ YXN0ZXIvREM9amFtZXNyb2JlcnRzb24vREM9ZXUwHhcNMTkwNTAyMTcwMDQ0WhcN
15
+ MjAwNTAxMTcwMDQ0WjAsMSowKAYDVQQDDCFnZW1tYXN0ZXIvREM9amFtZXNyb2Jl
16
+ cnRzb24vREM9ZXUwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQC2fJlK
17
+ qfMHZ4rXan+xQYSagEn9/o0es8LCLchPsC3pUzBRx5fBQcuy1ObfkUP8GgwTuDNk
18
+ jn6VL7rAqaNcLtWT5iizS9Ed0BJAYua8tcO9iHw2SXio1Ua8PGAuJNTc0y1Y6Pf7
19
+ r7p3+EJzdCplqc2wJCLJUO6rkS/QV6Lqh3EnOizEA5Z5FELMsnFiAM693IhekwBa
20
+ T8IJBrKKQt3+3S90LPppDcs1aufyujRW4RkF0s2CY1zg4IEC5W2il1Zvr0a6vi91
21
+ ckwI5MLweRqFgbjA6PbwMp5LX07nyNHSbI5iX+bHti0AJygaRYDM4xhyapLg6HJG
22
+ T5Y6b57B9bFIAzTHiGgN0hyFF7HMwMqnDsus0cVFX5EbVLbGR815dos7nq1P6ly1
23
+ SbXwHTTbCuzz+VJa4vYljVoMDIbhzWzwfubO3mfvysDJkFAUlvHeNMu9HrQNJHIB
24
+ C7+ef+I7v/QCKalG61u7DblbQd8TPWuo6ju+WY04rkB/0Vy0yzurgqKRAmMCAwEA
25
+ AaOBijCBhzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU8Ed9rlJ3
26
+ kQBOovH/VeIBw1e0KykwJgYDVR0RBB8wHYEbZ2VtbWFzdGVyQGphbWVzcm9iZXJ0
27
+ c29uLmV1MCYGA1UdEgQfMB2BG2dlbW1hc3RlckBqYW1lc3JvYmVydHNvbi5ldTAN
28
+ BgkqhkiG9w0BAQsFAAOCAYEAL5pgldzt710r9hbfp7qmiXMnFOD02LSO6JSQ+G5w
29
+ wrSr/cXxGVXkhnFdjfNukorbBujDue/HwEr6A2ZlC+hWPlbjBLA6TVS2rkkhOBxv
30
+ XfPqNW1PnYajqUhKKCd5ia0OUXdlfK1+/ck/lfRxozFYJ67TfWIrD5jgVUBNYKZc
31
+ PTiJfOU9yQink3hp9QKU5ROVQnMbN+4lB9VUw4EGF9Dl3ZD7KDCMT/a4FEFTNT4V
32
+ UkDwGqcuwVbTsnqZdK6e2j7m3r8IjCcB0iUAcHPWIeCQPfkZONOL1w/tNAxDvYGA
33
+ c4MxN53yGj29Kce6sE2ACO+K+X4zeU074gVAbX4qSGUSkfvgYArRq4iJe0uaoWs3
34
+ sPSYF/gqHJoyNtPGc3qBwBSavqr0zhCtc8jki4FELOmMmJOqrmupeUjWJqzYUW2K
35
+ iJVbOvYHXer/n2nZUT7Y3p/QDtueJZnnK3YK+ATp5fv8qdUhyMKlbzNLK4n6cZAm
36
+ DdWGleqfN3lBh5+Kul0GL3kB
37
+ -----END CERTIFICATE-----
38
+ date: 2019-05-02 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: subunit
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.3.0
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '0.3'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 0.3.0
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.3'
60
+ - !ruby/object:Gem::Dependency
61
+ name: ruby-ogginfo
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.7'
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 0.7.2
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.7'
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 0.7.2
80
+ - !ruby/object:Gem::Dependency
81
+ name: wavefile
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.1.0
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.1.0
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1.1'
100
+ - !ruby/object:Gem::Dependency
101
+ name: archive-zip
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 0.12.0
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.12'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 0.12.0
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '0.12'
120
+ - !ruby/object:Gem::Dependency
121
+ name: pollyspeech
122
+ requirement: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 0.2.0
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '0.2'
130
+ type: :runtime
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 0.2.0
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '0.2'
140
+ description:
141
+ email: james@jamesrobertson.eu
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - lib/v2av.rb
147
+ homepage: https://github.com/jrobertson/v2av
148
+ licenses:
149
+ - MIT
150
+ metadata: {}
151
+ post_install_message:
152
+ rdoc_options: []
153
+ require_paths:
154
+ - lib
155
+ required_ruby_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ requirements: []
166
+ rubygems_version: 3.0.1
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Adds subtitles + TTS voiceover to video.
170
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ z����D�M��#�=���B4��JH��2��y�������wE;��u�xٷ 6����Z��k�]��fq��:2T��x�,w2o�o�I� ��'Z���)K �wx~7���i��׺�m���(��܊~��G�����T�`�t�۽R��~,w[��Q��~��H��sԥ�WAee*��euϊ�@�\��l��굲�av�� ��s|���dp�|��l�25��f� ��m����rVx���V\�-�����g\9\}f���C�Q}�q����ã�X����^�.$�C2B��K������
2
+ =V��(3zX��I;W�����$KB��(*\Mf&&�:R�x�KA(���Ղ=��kt�@�j�����u