Class: Storyboard
- Defined in:
- lib/storyboard.rb,
lib/storyboard/cache.rb,
lib/storyboard/version.rb,
lib/storyboard/bincheck.rb,
lib/storyboard/subtitles.rb,
lib/storyboard/generators/gif.rb,
lib/storyboard/generators/pdf.rb,
lib/storyboard/generators/sub.rb
Direct Known Subclasses
Defined Under Namespace
Classes: Cache, GifRenderer, PDFRenderer, Renderer, SRT
Constant Summary collapse
- VERSION =
"0.5.1"
Instance Attribute Summary collapse
-
#cache ⇒ Object
Returns the value of attribute cache.
-
#capture_points ⇒ Object
Returns the value of attribute capture_points.
-
#encoding ⇒ Object
Returns the value of attribute encoding.
-
#length ⇒ Object
Returns the value of attribute length.
-
#mime ⇒ Object
Returns the value of attribute mime.
-
#needs_KFhimaji ⇒ Object
Returns the value of attribute needs_KFhimaji.
-
#options ⇒ Object
Returns the value of attribute options.
-
#renderers ⇒ Object
Returns the value of attribute renderers.
-
#subtitles ⇒ Object
Returns the value of attribute subtitles.
-
#timings ⇒ Object
Returns the value of attribute timings.
Class Method Summary collapse
- .current_encoding ⇒ Object
- .current_encoding=(n) ⇒ Object
- .encode_regexp(r) ⇒ Object
- .encode_string(r) ⇒ Object
- .ffprobe_installed? ⇒ Boolean
- .magick_installed? ⇒ Boolean
- .mkvtools_installed? ⇒ Boolean
- .mp4box_insatlled? ⇒ Boolean
- .needs_KFhimaji(set = false) ⇒ Object
- .path ⇒ Object
Instance Method Summary collapse
- #check_video ⇒ Object
- #cleanup ⇒ Object
- #consolidate_frames ⇒ Object
- #extract_frames ⇒ Object
- #get_subtitles ⇒ Object
-
#initialize(o) ⇒ Storyboard
constructor
A new instance of Storyboard.
- #mkv? ⇒ Boolean
- #render_output ⇒ Object
- #run ⇒ Object
- #run_scene_detection ⇒ Object
- #setup ⇒ Object
- #video_file? ⇒ Boolean
Constructor Details
#initialize(o) ⇒ Storyboard
Returns a new instance of Storyboard.
26 27 28 29 30 31 32 33 |
# File 'lib/storyboard.rb', line 26 def initialize(o) @needs_KFhimaji = false @capture_points = [] @renderers = [] @options = o @encoding = "UTF-8" check_video end |
Instance Attribute Details
#cache ⇒ Object
Returns the value of attribute cache.
23 24 25 |
# File 'lib/storyboard.rb', line 23 def cache @cache end |
#capture_points ⇒ Object
Returns the value of attribute capture_points.
22 23 24 |
# File 'lib/storyboard.rb', line 22 def capture_points @capture_points end |
#encoding ⇒ Object
Returns the value of attribute encoding.
23 24 25 |
# File 'lib/storyboard.rb', line 23 def encoding @encoding end |
#length ⇒ Object
Returns the value of attribute length.
23 24 25 |
# File 'lib/storyboard.rb', line 23 def length @length end |
#mime ⇒ Object
Returns the value of attribute mime.
23 24 25 |
# File 'lib/storyboard.rb', line 23 def mime @mime end |
#needs_KFhimaji ⇒ Object
Returns the value of attribute needs_KFhimaji.
24 25 26 |
# File 'lib/storyboard.rb', line 24 def needs_KFhimaji @needs_KFhimaji end |
#options ⇒ Object
Returns the value of attribute options.
22 23 24 |
# File 'lib/storyboard.rb', line 22 def @options end |
#renderers ⇒ Object
Returns the value of attribute renderers.
23 24 25 |
# File 'lib/storyboard.rb', line 23 def renderers @renderers end |
#subtitles ⇒ Object
Returns the value of attribute subtitles.
22 23 24 |
# File 'lib/storyboard.rb', line 22 def subtitles @subtitles end |
#timings ⇒ Object
Returns the value of attribute timings.
22 23 24 |
# File 'lib/storyboard.rb', line 22 def timings @timings end |
Class Method Details
.current_encoding ⇒ Object
43 44 45 |
# File 'lib/storyboard.rb', line 43 def self.current_encoding @encoding || 'UTF-8' end |
.current_encoding=(n) ⇒ Object
47 48 49 |
# File 'lib/storyboard.rb', line 47 def self.current_encoding=(n) @encoding = n end |
.encode_regexp(r) ⇒ Object
51 52 53 |
# File 'lib/storyboard.rb', line 51 def self.encode_regexp(r) Regexp.new(r.encode(Storyboard.current_encoding), 16) end |
.encode_string(r) ⇒ Object
55 56 57 |
# File 'lib/storyboard.rb', line 55 def self.encode_string(r) r.encode(Storyboard.current_encoding) end |
.ffprobe_installed? ⇒ Boolean
21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/storyboard/bincheck.rb', line 21 def self.ffprobe_installed? good = command?("ffprobe") if good version = `ffprobe -version`.scan(/version ([\d\.]+)\n/) if version.empty? good = false else float_version = version.first[0].to_f good = float_version >= 1.1 end end return good end |
.magick_installed? ⇒ Boolean
17 18 19 |
# File 'lib/storyboard/bincheck.rb', line 17 def self.magick_installed? command?("mogrify") end |
.mkvtools_installed? ⇒ Boolean
9 10 11 |
# File 'lib/storyboard/bincheck.rb', line 9 def self.mkvtools_installed? command?("mkvextract") && command?("mkvmerge") end |
.mp4box_insatlled? ⇒ Boolean
13 14 15 |
# File 'lib/storyboard/bincheck.rb', line 13 def self.mp4box_insatlled? command?("MP4Box") end |
.needs_KFhimaji(set = false) ⇒ Object
35 36 37 |
# File 'lib/storyboard.rb', line 35 def self.needs_KFhimaji(set = false) @needs_KFhimaji ||= set end |
.path ⇒ Object
39 40 41 |
# File 'lib/storyboard.rb', line 39 def self.path File.dirname(__FILE__) + '/../' end |
Instance Method Details
#check_video ⇒ Object
170 171 172 173 174 175 176 |
# File 'lib/storyboard.rb', line 170 def check_video @mime = MIME::Types.type_for([:file]) if video_file? @length = `ffmpeg -i "#{[:file]}" 2>&1 | grep "Duration" | cut -d ' ' -f 4 | sed s/,//` @length = STRTime.parse(length.strip+'0').value end end |
#cleanup ⇒ Object
195 196 197 |
# File 'lib/storyboard.rb', line 195 def cleanup FileUtils.remove_dir(@options[:work_dir]) end |
#consolidate_frames ⇒ Object
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/storyboard.rb', line 110 def consolidate_frames @subtitles.pages.each {|f| @capture_points << f[:start_time] } @capture_points = @capture_points.sort_by {|cp| cp.value } last_time = STRTime.new(0) removed = 0 @capture_points.each_with_index {|ts,i| # while it should be a super rare condition, this should not be # allowed to delete subtitle frames. if (ts.value - last_time.value) < [:consolidate_frame_threshold] @capture_points.delete_at(i-1) unless i == 0 removed += 1 end last_time = ts } LOG.debug("Removed #{removed} frames that were within the consolidate_frame_threshold of #{[:consolidate_frame_threshold]}") end |
#extract_frames ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/storyboard.rb', line 127 def extract_frames pool = Thread::Pool.new(2) = ProgressBar.create(:title => " Extracting Frames", :format => '%t [%c/%C|%B] %e', :total => @stop_frame ) @capture_points.each_with_index {|f,i| if i >= @stop_frame pool.shutdown return end # It's *massively* quicker to jump to a bit before where we want to be, and then make the incrimental jump to # exactly where we want to be. seek_primer = (f.value < 1.000) ? 0 : -1.000 # should make Frame a struct with idx and subs image_name = File.join(@options[:save_directory], "%04d.jpg" % [i]) pool.process { .increment cmd = ["ffmpeg", "-ss", (f + seek_primer).to_srt, "-i", %("#{[:file]}"), "-vframes 1", "-ss", STRTime.new(seek_primer.abs).to_srt, %("#{image_name}")].join(' ') Open3.popen3(cmd){|stdin, stdout, stderr, wait_thr| # block the output so it doesn't quit immediately stdout.readlines } } } pool.shutdown LOG.info("Finished Extracting Frames") end |
#get_subtitles ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/storyboard/subtitles.rb', line 4 def get_subtitles extensionless = File.join(File.dirname([:file]), File.basename([:file], ".*") + '.srt') if mkv? if Storyboard.mkvtools_installed? output = `mkvmerge -i "#{[:file]}"` subs = output.scan(/Track ID (\d+): subtitles \(S_TEXT\/UTF8\)/) # until I can play with better output, take the first. if not subs.empty? LOG.info("Extracting embedded subtitles") LOG.info("Multiple subtitles found in the mkv. Taking the first.") if subs.count > 1 `mkvextract tracks "#{[:file]}" #{subs.first[0]}:"#{[:work_dir]}/subtitles.srt"` return File.read("#{[:work_dir]}/subtitles.srt", ) end else LOG.debug("File is mkv, but no mkvtoolnix installed.") end end if File.exists?([:file] + '.srt') return File.read([:file] + '.srt') elsif File.exists?(extensionless) return File.read(extensionless) end # suby includes a giant util library the guy also wrote # that it uses to call file.basename instead of File.basename(file), #but "file" has to be a "Path", so, whatever. suby_file = Path([:file]) downloader = Suby::Downloader::OpenSubtitles.new(suby_file, 'en') chosen = nil if @cache.subtitles.nil? LOG.debug("No subtitles cache found") @cache.subtitles = downloader.possible_urls @cache.save end chosen = pick_best_subtitle(@cache.subtitles) contents = @cache.download_file(chosen) do downloader.extract(chosen) end contents end |
#mkv? ⇒ Boolean
182 183 184 |
# File 'lib/storyboard.rb', line 182 def mkv? !@mime.grep(/matroska/).empty? end |
#render_output ⇒ Object
156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/storyboard.rb', line 156 def render_output = ProgressBar.create(:title => " Rendering Output", :format => '%t [%c/%C|%B] %e', :total => ( @options[:preview] || @capture_points.count )) @capture_points.each_with_index {|f,i| next if i >= @stop_frame image_name = File.join(@options[:save_directory], "%04d.jpg" % [i]) capture_point_subtitles = @subtitles.pages.select { |page| f.value >= page.start_time.value and f.value <= page.end_time.value }.last @renderers.each{|r| r.render_frame(image_name, capture_point_subtitles) } .increment } @renderers.each {|r| r.write } LOG.info("Finished Rendering Output files") end |
#run ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/storyboard.rb', line 59 def run LOG.info("Processing #{[:file]}") setup @cache = Cache.new(Suby::MovieHasher.compute_hash(Path.new([:file]))) LOG.debug() if [:verbose] @subtitles = SRT.new([:subs] ? File.read([:subs]) : get_subtitles, ) # bit of a temp hack so I don't have to wait all the time. @subtitles.save if [:verbose] @cache.save @renderers << Storyboard::PDFRenderer.new(self) if [:types].include?('pdf') run_scene_detection if [:scenes] consolidate_frames # If the preview flag is set, do a quick count of how many subtitled frames fall before it. if @options[:preview] stop_time = @subtitles.pages[@options[:preview]].start_time.value @stop_frame = @capture_points.count {|f| stop_time > f.value } else @stop_frame = @capture_points.count end extract_frames render_output exit cleanup end |
#run_scene_detection ⇒ Object
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/storyboard.rb', line 94 def run_scene_detection = ProgressBar.create(:title => " Analyzing Video", :format => '%t [%B] %e', :total => @length, :smoothing => 0.6) bin = File.join(File.dirname(__FILE__), '../bin/storyboard-ffprobe') Open3.popen3('ffprobe', "-show_frames", "-of", "compact=p=0", "-f", "lavfi", %(movie=#{[:file]},select=gt(scene\\,.30)), "-pretty") {|stdin, stdout, stderr, wait_thr| begin # trolololol o = stdout.gets.split('|').inject({}){|hold,value| s = value.split('='); hold[s[0]]=s[1]; hold } t = STRTime.parse(o['pkt_pts_time'], true) .progress = t.value @capture_points << t end while !stdout.eof? } .finish LOG.info("#{@capture_points.count} scenes found and added to the timeline") end |
#setup ⇒ Object
186 187 188 189 190 191 192 193 |
# File 'lib/storyboard.rb', line 186 def setup @options[:basename] = File.basename([:file], ".*") @options[:work_dir] = Dir.mktmpdir raise "Unable to create temporary directory" unless File.directory?(@options[:work_dir]) Dir.mkdir(@options[:write_to]) unless File.directory?(@options[:write_to]) @options[:save_directory] = File.join(@options[:work_dir], 'raw_frames') Dir.mkdir(@options[:save_directory]) unless File.directory?(@options[:save_directory]) end |
#video_file? ⇒ Boolean
178 179 180 |
# File 'lib/storyboard.rb', line 178 def video_file? !@mime.grep(/video\//).empty? end |