storyboard 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +16 -0
- data/Gemfile.lock +1 -1
- data/README.md +9 -9
- data/bin/storyboard +22 -5
- data/lib/storyboard/subtitles.rb +3 -8
- data/lib/storyboard/version.rb +1 -1
- data/lib/storyboard.rb +21 -4
- data/setup/osx.sh +1 -1
- metadata +150 -133
data/CHANGELOG
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
0.4.0
|
2
|
+
|
3
|
+
Added two new options to Storyboard:
|
4
|
+
--preview [NUMBER] will output only the first NUMBER of frames that have subtitles.
|
5
|
+
if --preview is used with -c (forcing a scene change scan), it may include more than
|
6
|
+
NUMBER of frames, as the scene changes will also be included.
|
7
|
+
|
8
|
+
-n [NUMBER] nudges the timestamps forward or back in time by NUMBER seconds.
|
9
|
+
Combined with --preview this lets you more quickly line up the subtitles. Error detection
|
10
|
+
on nudging is nonexistant, and so if you nudge them too far forward or back they could
|
11
|
+
cause an error.
|
12
|
+
|
13
|
+
The following example will move the subtitles back 2 seconds, and render a 25 frame PDF:
|
14
|
+
|
15
|
+
storyboard -n -2 --preview 25 Video.mkv
|
16
|
+
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -18,17 +18,17 @@ Storyboard will try to generate a file at `/path/to/video-file.pdf` containing t
|
|
18
18
|
$ ls .
|
19
19
|
ShowName.1x01.EpisodeName.pdf ShowName.1x02.AnotherEpisode.pdf ShowName.1x03.ThirdEp.pdf
|
20
20
|
|
21
|
-
|
21
|
+
To quickly test if the subtitles that are used look ok, you can use the `--preview NUMBER` option, which generates a PDF with as many pages as you specify, defaulting to 10.
|
22
22
|
|
23
|
-
|
24
|
-
-v, --[no-]verbose Run verbosely
|
25
|
-
--[no-]scenes Detect scene changes. This increases the time it takes to generate a file.
|
26
|
-
-ct FLOAT Scene detection threshold. 0.2 is too low, 0.8 is too high. Play with it!
|
27
|
-
-s, --subs FILE SRT subtitle file to use. Will skip extracting/downloading one.
|
28
|
-
--make x,y,z Filetypes to output
|
29
|
-
(pdf, mobi, epub)
|
30
|
-
-h, --help Show this message
|
23
|
+
storyboard --preview /path/to/video-file.mkv
|
31
24
|
|
25
|
+
If the subtitles are off, you can nudge them back or forward with the `-n TIME` option. This can be positive or negative, and if you make it too large it can cause Storyboard to throw an error. This would nudge the subtitles back 2 seconds, and generate just the preview PDF.
|
26
|
+
|
27
|
+
storyboard -n -2 --preview /path/to/video-file.mkv
|
28
|
+
|
29
|
+
You can see all the available options by using the help option:
|
30
|
+
|
31
|
+
storyboard -h
|
32
32
|
|
33
33
|
## Requirements
|
34
34
|
|
data/bin/storyboard
CHANGED
@@ -22,12 +22,14 @@ unless Storyboard.magick_installed?
|
|
22
22
|
exit
|
23
23
|
end
|
24
24
|
|
25
|
-
options = {:
|
25
|
+
options = {:nudge => 0, :verbose => true, :types => ['pdf'], :scene_threshold => 0.4}
|
26
26
|
|
27
27
|
# think about them
|
28
28
|
options[:consolidate_frame_threshold] = 0.4
|
29
29
|
options[:max_width] = 600
|
30
30
|
|
31
|
+
puts "Running Storyboard #{Storyboard::VERSION}"
|
32
|
+
|
31
33
|
LOG = Logger.new(STDOUT)
|
32
34
|
LOG.level = Logger::INFO
|
33
35
|
|
@@ -43,16 +45,27 @@ opts.on("-c", "--[no-]scenes", "Detect scene changes. This increases the time it
|
|
43
45
|
options[:scenes] = v
|
44
46
|
end
|
45
47
|
|
46
|
-
opts.on("
|
47
|
-
options[:
|
48
|
+
opts.on("--preview [FRAMES]", Integer, "Only render the specified number (default 10) of subtitled images.", "Unless the -c flag is used, scene detection will be skipped") do |n|
|
49
|
+
options[:preview] = n || 10
|
50
|
+
options[:scenes] ||= false
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("-n", "--nudge TIME", Float, "Nudge the subtitles forward or backward. TIME is the number of seconds.", "Use this with the --preview option to quickly check and adjust the subtitle timings.") do |time|
|
54
|
+
options[:nudge] = time
|
48
55
|
end
|
49
56
|
|
57
|
+
#opts.on("--make x,y,z", Array, "Filetypes to output", '(pdf, mobi, epub)') do |types|
|
58
|
+
# options[:types] = types
|
59
|
+
#end
|
60
|
+
|
50
61
|
opts.on("-s", "--subs FILE", "SRT subtitle file to use. Will skip extracting/downloading one.") do |s|
|
51
62
|
options[:subs] = s
|
52
63
|
end
|
53
64
|
|
54
|
-
opts.on(
|
55
|
-
|
65
|
+
opts.on('--update', "Update Storyboard to the latest available version") do |u|
|
66
|
+
# This execs in the current environment, I believe.
|
67
|
+
puts `gem update storyboard`
|
68
|
+
exit
|
56
69
|
end
|
57
70
|
|
58
71
|
opts.on_tail("-h", "--help", "Show this message") do
|
@@ -67,6 +80,10 @@ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
|
|
67
80
|
exit 1
|
68
81
|
end
|
69
82
|
|
83
|
+
options[:scenes] = true if options[:scenes].nil?
|
84
|
+
|
85
|
+
p options
|
86
|
+
|
70
87
|
if ARGV.size < 1
|
71
88
|
puts "videofile required"
|
72
89
|
puts opts.to_s
|
data/lib/storyboard/subtitles.rb
CHANGED
@@ -35,12 +35,7 @@ class Storyboard
|
|
35
35
|
#downloader = Suby::Downloader::OpenSubtitles.new(suby_file, 'en')
|
36
36
|
# try Addic7ed first, as, on average, it seems a bit better.
|
37
37
|
downloader = nil
|
38
|
-
|
39
|
-
LOG.debug("Searching for subtitles on Addic7ed")
|
40
|
-
#downloader = Suby::Downloader::Addic7ed.new(suby_file, 'en')
|
41
|
-
rescue Exception => e
|
42
|
-
LOG.debug(e)
|
43
|
-
end
|
38
|
+
|
44
39
|
|
45
40
|
if downloader.nil?
|
46
41
|
LOG.info("Searching for subtitles on OpenSubtitles")
|
@@ -89,8 +84,8 @@ class Storyboard
|
|
89
84
|
end
|
90
85
|
when :time
|
91
86
|
if l =~ /^(#{TIME_REGEX}) --> (#{TIME_REGEX})$/
|
92
|
-
page[:start_time] = STRTime.parse($1)
|
93
|
-
page[:end_time] = STRTime.parse($2)
|
87
|
+
page[:start_time] = STRTime.parse($1) + @options[:nudge]
|
88
|
+
page[:end_time] = STRTime.parse($2) + @options[:nudge]
|
94
89
|
phase = :text
|
95
90
|
else
|
96
91
|
raise "Bad SRT File: Should have time range but got '#{l}'"
|
data/lib/storyboard/version.rb
CHANGED
data/lib/storyboard.rb
CHANGED
@@ -34,12 +34,22 @@ class Storyboard
|
|
34
34
|
# bit of a temp hack so I don't have to wait all the time.
|
35
35
|
@subtitles.save if options[:verbose]
|
36
36
|
|
37
|
-
|
38
37
|
@renderers << Storyboard::PDFRenderer.new(self) if options[:types].include?('pdf')
|
39
38
|
|
40
39
|
run_scene_detection if options[:scenes]
|
41
40
|
consolidate_frames
|
41
|
+
|
42
|
+
|
43
|
+
# If the preview flag is set, do a quick count of how many subtitled frames fall before it.
|
44
|
+
if @options[:preview]
|
45
|
+
stop_time = @subtitles.pages[@options[:preview]].start_time.value
|
46
|
+
@stop_frame = @capture_points.count {|f| stop_time > f.value }
|
47
|
+
else
|
48
|
+
@stop_frame = @capture_points.count
|
49
|
+
end
|
50
|
+
|
42
51
|
extract_frames
|
52
|
+
|
43
53
|
render_output
|
44
54
|
|
45
55
|
cleanup
|
@@ -58,7 +68,7 @@ class Storyboard
|
|
58
68
|
end while !stdout.eof?
|
59
69
|
}
|
60
70
|
pbar.finish
|
61
|
-
LOG.info("#{@capture_points.count} scenes
|
71
|
+
LOG.info("#{@capture_points.count} scenes found and added to the timeline")
|
62
72
|
end
|
63
73
|
|
64
74
|
def consolidate_frames
|
@@ -80,9 +90,14 @@ class Storyboard
|
|
80
90
|
|
81
91
|
def extract_frames
|
82
92
|
pool = Thread::Pool.new(2)
|
83
|
-
pbar = ProgressBar.create(:title => " Extracting Frames", :format => '%t [%c/%C|%B] %e', :total => @
|
93
|
+
pbar = ProgressBar.create(:title => " Extracting Frames", :format => '%t [%c/%C|%B] %e', :total => @stop_frame )
|
84
94
|
|
85
95
|
@capture_points.each_with_index {|f,i|
|
96
|
+
if i >= @stop_frame
|
97
|
+
pool.shutdown
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
86
101
|
# It's *massively* quicker to jump to a bit before where we want to be, and then make the incrimental jump to
|
87
102
|
# exactly where we want to be.
|
88
103
|
seek_primer = (f.value < 1.000) ? 0 : -1.000
|
@@ -103,8 +118,9 @@ class Storyboard
|
|
103
118
|
end
|
104
119
|
|
105
120
|
def render_output
|
106
|
-
pbar = ProgressBar.create(:title => " Rendering Output", :format => '%t [%c/%C|%B] %e', :total => @capture_points.count)
|
121
|
+
pbar = ProgressBar.create(:title => " Rendering Output", :format => '%t [%c/%C|%B] %e', :total => ( @options[:preview] || @capture_points.count ))
|
107
122
|
@capture_points.each_with_index {|f,i|
|
123
|
+
next if i >= @stop_frame
|
108
124
|
image_name = File.join(@options[:save_directory], "%04d.jpg" % [i])
|
109
125
|
capture_point_subtitles = @subtitles.pages.select { |page| f.value >= page.start_time.value and f.value <= page.end_time.value }.first
|
110
126
|
begin
|
@@ -138,6 +154,7 @@ class Storyboard
|
|
138
154
|
def setup
|
139
155
|
@options[:basename] = File.basename(options[:file], ".*")
|
140
156
|
@options[:work_dir] = Dir.mktmpdir
|
157
|
+
raise "Unable to create temporary directory" unless File.directory?(@options[:work_dir])
|
141
158
|
Dir.mkdir(@options[:write_to]) unless File.directory?(@options[:write_to])
|
142
159
|
@options[:save_directory] = File.join(@options[:work_dir], 'raw_frames')
|
143
160
|
Dir.mkdir(@options[:save_directory]) unless File.directory?(@options[:save_directory])
|
data/setup/osx.sh
CHANGED
@@ -78,7 +78,7 @@ if [ -x /usr/local/bin/convert ]; then
|
|
78
78
|
printf '%s ok %s\n' "$(tput setaf 2)" "$(tput op)"
|
79
79
|
else
|
80
80
|
printf '%s have to install %s\n' "$(tput setaf 1)" "$(tput op)"
|
81
|
-
brew install imagemagick
|
81
|
+
brew install imagemagick libtool
|
82
82
|
fi
|
83
83
|
|
84
84
|
echo -n "Checking for Ghostscript"
|
metadata
CHANGED
@@ -1,145 +1,171 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: storyboard
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 3
|
8
|
-
- 1
|
9
|
-
version: 0.3.1
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
prerelease:
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Mark Olson
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
dependencies:
|
20
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2013-01-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
21
15
|
name: nokogiri
|
22
|
-
|
23
|
-
|
24
|
-
requirements:
|
25
|
-
- -
|
26
|
-
- !ruby/object:Gem::Version
|
27
|
-
|
28
|
-
- 0
|
29
|
-
version: "0"
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
30
22
|
type: :runtime
|
31
|
-
version_requirements: *id001
|
32
|
-
- !ruby/object:Gem::Dependency
|
33
|
-
name: mini_magick
|
34
23
|
prerelease: false
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: mini_magick
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
42
38
|
type: :runtime
|
43
|
-
version_requirements: *id002
|
44
|
-
- !ruby/object:Gem::Dependency
|
45
|
-
name: prawn
|
46
39
|
prerelease: false
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: prawn
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
54
|
type: :runtime
|
55
|
-
version_requirements: *id003
|
56
|
-
- !ruby/object:Gem::Dependency
|
57
|
-
name: ruby-progressbar
|
58
55
|
prerelease: false
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: ruby-progressbar
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
66
70
|
type: :runtime
|
67
|
-
version_requirements: *id004
|
68
|
-
- !ruby/object:Gem::Dependency
|
69
|
-
name: levenshtein-ffi
|
70
71
|
prerelease: false
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: levenshtein-ffi
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
78
86
|
type: :runtime
|
79
|
-
version_requirements: *id005
|
80
|
-
- !ruby/object:Gem::Dependency
|
81
|
-
name: path
|
82
87
|
prerelease: false
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: path
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
91
101
|
version: 1.3.0
|
92
102
|
type: :runtime
|
93
|
-
version_requirements: *id006
|
94
|
-
- !ruby/object:Gem::Dependency
|
95
|
-
name: rubyzip
|
96
103
|
prerelease: false
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.3.0
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rubyzip
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
104
118
|
type: :runtime
|
105
|
-
version_requirements: *id007
|
106
|
-
- !ruby/object:Gem::Dependency
|
107
|
-
name: term-ansicolor
|
108
119
|
prerelease: false
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: term-ansicolor
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
116
134
|
type: :runtime
|
117
|
-
version_requirements: *id008
|
118
|
-
- !ruby/object:Gem::Dependency
|
119
|
-
name: mime-types
|
120
135
|
prerelease: false
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: mime-types
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '1.19'
|
129
150
|
type: :runtime
|
130
|
-
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '1.19'
|
131
158
|
description: Generate PDFs and eBooks from video files
|
132
|
-
email:
|
133
|
-
- "
|
134
|
-
executables:
|
159
|
+
email:
|
160
|
+
- ! '"theothermarkolson@gmail.com"'
|
161
|
+
executables:
|
135
162
|
- storyboard
|
136
163
|
extensions: []
|
137
|
-
|
138
164
|
extra_rdoc_files: []
|
139
|
-
|
140
|
-
files:
|
165
|
+
files:
|
141
166
|
- .gitignore
|
142
167
|
- .rvmrc
|
168
|
+
- CHANGELOG
|
143
169
|
- Gemfile
|
144
170
|
- Gemfile.lock
|
145
171
|
- INSTALL.md
|
@@ -179,38 +205,29 @@ files:
|
|
179
205
|
- vendor/suby/spec/suby/filename_parser_spec.rb
|
180
206
|
- vendor/suby/spec/suby_spec.rb
|
181
207
|
- vendor/suby/suby.gemspec
|
182
|
-
has_rdoc: true
|
183
208
|
homepage: http://github.com/markolson/storyboard
|
184
209
|
licenses: []
|
185
|
-
|
186
210
|
post_install_message:
|
187
211
|
rdoc_options: []
|
188
|
-
|
189
|
-
require_paths:
|
212
|
+
require_paths:
|
190
213
|
- lib
|
191
214
|
- vendor/suby
|
192
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
- 1
|
198
|
-
- 9
|
199
|
-
- 3
|
215
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
216
|
+
none: false
|
217
|
+
requirements:
|
218
|
+
- - ! '>='
|
219
|
+
- !ruby/object:Gem::Version
|
200
220
|
version: 1.9.3
|
201
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
version: "0"
|
221
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
222
|
+
none: false
|
223
|
+
requirements:
|
224
|
+
- - ! '>='
|
225
|
+
- !ruby/object:Gem::Version
|
226
|
+
version: '0'
|
208
227
|
requirements: []
|
209
|
-
|
210
228
|
rubyforge_project:
|
211
|
-
rubygems_version: 1.
|
229
|
+
rubygems_version: 1.8.23
|
212
230
|
signing_key:
|
213
231
|
specification_version: 3
|
214
232
|
summary: Video to PDF/ePub generator
|
215
233
|
test_files: []
|
216
|
-
|