v2av 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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