screenkit 0.0.0 → 0.0.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docker.yml +44 -0
  3. data/.github/workflows/ruby-tests.yml +21 -1
  4. data/Brewfile +5 -0
  5. data/CHANGELOG.md +13 -1
  6. data/CONTRIBUTING.md +25 -0
  7. data/DOCUMENTATION.md +132 -48
  8. data/Dockerfile +107 -0
  9. data/lib/screen_kit.rb +17 -16
  10. data/lib/screenkit/animation_filters.rb +16 -0
  11. data/lib/screenkit/callout/styles/base.rb +12 -0
  12. data/lib/screenkit/callout/styles/file_copy.rb +26 -0
  13. data/lib/screenkit/callout/styles/inline_block.rb +6 -9
  14. data/lib/screenkit/callout/styles/{default.rb → shadow_block.rb} +9 -11
  15. data/lib/screenkit/callout.rb +1 -1
  16. data/lib/screenkit/cli/episode.rb +1 -1
  17. data/lib/screenkit/config/episode.rb +6 -0
  18. data/lib/screenkit/config/project.rb +5 -2
  19. data/lib/screenkit/duration.rb +5 -0
  20. data/lib/screenkit/exporter/demotape.rb +20 -2
  21. data/lib/screenkit/exporter/episode.rb +42 -16
  22. data/lib/screenkit/exporter/intro.rb +4 -4
  23. data/lib/screenkit/exporter/outro.rb +5 -5
  24. data/lib/screenkit/exporter/segment.rb +270 -74
  25. data/lib/screenkit/generators/episode/callouts/001.yml +12 -0
  26. data/lib/screenkit/generators/episode/config.yml.erb +8 -8
  27. data/lib/screenkit/generators/project/screenkit.yml +123 -27
  28. data/lib/screenkit/schema_validator.rb +1 -1
  29. data/lib/screenkit/schemas/callout_styles/file_copy.json +8 -0
  30. data/lib/screenkit/schemas/callout_styles/inline_block.json +20 -0
  31. data/lib/screenkit/schemas/{callouts/default.json → callout_styles/shadow_block.json} +2 -2
  32. data/lib/screenkit/schemas/callouts/inline_block.json +20 -11
  33. data/lib/screenkit/schemas/callouts/shadow_block.json +39 -0
  34. data/lib/screenkit/schemas/episode.json +7 -55
  35. data/lib/screenkit/schemas/project.json +6 -6
  36. data/lib/screenkit/schemas/refs/anchor.json +1 -1
  37. data/lib/screenkit/schemas/refs/animation.json +1 -1
  38. data/lib/screenkit/schemas/refs/background.json +1 -1
  39. data/lib/screenkit/schemas/refs/{callout.json → callout_style.json} +6 -8
  40. data/lib/screenkit/schemas/refs/color.json +1 -1
  41. data/lib/screenkit/schemas/refs/demotape.json +48 -0
  42. data/lib/screenkit/schemas/refs/demotape_themes.json +367 -0
  43. data/lib/screenkit/schemas/refs/directory.json +1 -1
  44. data/lib/screenkit/schemas/refs/duration.json +9 -0
  45. data/lib/screenkit/schemas/refs/intro.json +4 -16
  46. data/lib/screenkit/schemas/refs/logo.json +1 -1
  47. data/lib/screenkit/schemas/refs/outro.json +4 -16
  48. data/lib/screenkit/schemas/refs/position.json +1 -1
  49. data/lib/screenkit/schemas/refs/scenes.json +2 -6
  50. data/lib/screenkit/schemas/refs/size.json +1 -1
  51. data/lib/screenkit/schemas/refs/sound.json +1 -1
  52. data/lib/screenkit/schemas/refs/spacing.json +1 -1
  53. data/lib/screenkit/schemas/refs/text_style.json +1 -1
  54. data/lib/screenkit/schemas/refs/transition.json +2 -6
  55. data/lib/screenkit/schemas/refs/tts.json +4 -41
  56. data/lib/screenkit/schemas/refs/tts_builtin.json +23 -0
  57. data/lib/screenkit/schemas/refs/watermark.json +1 -1
  58. data/lib/screenkit/schemas/tts/elevenlabs.json +11 -2
  59. data/lib/screenkit/schemas/tts/espeak.json +26 -0
  60. data/lib/screenkit/schemas/tts/say.json +12 -2
  61. data/lib/screenkit/shell.rb +6 -0
  62. data/lib/screenkit/time_formatter.rb +14 -0
  63. data/lib/screenkit/tts/base.rb +21 -0
  64. data/lib/screenkit/tts/eleven_labs.rb +8 -9
  65. data/lib/screenkit/tts/espeak.rb +30 -0
  66. data/lib/screenkit/tts/say.rb +5 -6
  67. data/lib/screenkit/utils.rb +6 -0
  68. data/lib/screenkit/version.rb +1 -1
  69. metadata +29 -46
  70. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Bold.ttf +0 -0
  71. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-BoldItalic.ttf +0 -0
  72. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-ExtraBoldItalic.ttf +0 -0
  73. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Italic.ttf +0 -0
  74. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Light.ttf +0 -0
  75. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-LightItalic.ttf +0 -0
  76. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Medium.ttf +0 -0
  77. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-MediumItalic.ttf +0 -0
  78. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Regular.ttf +0 -0
  79. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-SemiBoldItalic.ttf +0 -0
  80. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Bold.ttf +0 -0
  81. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-BoldItalic.ttf +0 -0
  82. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBold.ttf +0 -0
  83. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBoldItalic.ttf +0 -0
  84. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Italic.ttf +0 -0
  85. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Light.ttf +0 -0
  86. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-LightItalic.ttf +0 -0
  87. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Medium.ttf +0 -0
  88. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-MediumItalic.ttf +0 -0
  89. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Regular.ttf +0 -0
  90. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBold.ttf +0 -0
  91. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBoldItalic.ttf +0 -0
  92. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Bold.ttf +0 -0
  93. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-BoldItalic.ttf +0 -0
  94. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBold.ttf +0 -0
  95. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBoldItalic.ttf +0 -0
  96. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Italic.ttf +0 -0
  97. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Light.ttf +0 -0
  98. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-LightItalic.ttf +0 -0
  99. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Medium.ttf +0 -0
  100. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-MediumItalic.ttf +0 -0
  101. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Regular.ttf +0 -0
  102. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBold.ttf +0 -0
  103. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  104. /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/OFL.txt +0 -0
  105. /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/OpenSans-ExtraBold.ttf +0 -0
  106. /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/OpenSans-SemiBold.ttf +0 -0
  107. /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/README.txt +0 -0
@@ -4,6 +4,18 @@ module ScreenKit
4
4
  class Callout
5
5
  module Styles
6
6
  class Base
7
+ attr_reader :source, :output_path, :log_path
8
+ attr_accessor :options
9
+
10
+ extend SchemaValidator
11
+
12
+ def initialize(source:, output_path:, log_path: nil, **options)
13
+ @source = source
14
+ @output_path = output_path
15
+ @log_path = log_path
16
+ @options = options
17
+ end
18
+
7
19
  def text_wrap(text, max_width:, font_size:)
8
20
  words = text.to_s.split(/\s+/)
9
21
  width_factor = 0.6
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScreenKit
4
+ class Callout
5
+ module Styles
6
+ class FileCopy < Base
7
+ def self.schema_path
8
+ ScreenKit.root_dir
9
+ .join("screenkit/schemas/callout_styles/file_copy.json")
10
+ end
11
+
12
+ def initialize(source:, **kwargs)
13
+ self.class.validate!(kwargs)
14
+ super
15
+ end
16
+
17
+ def render
18
+ ext = File.extname(options[:file_path])
19
+ FileUtils.mkdir_p(File.dirname(output_path))
20
+ FileUtils.cp source.search(options[:file_path]),
21
+ output_path.sub_ext(ext)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -6,30 +6,27 @@ module ScreenKit
6
6
  class Callout
7
7
  module Styles
8
8
  class InlineBlock < Base
9
- extend SchemaValidator
10
-
11
9
  attr_reader :background_color, :text_style, :body,
12
- :output_path, :padding, :text, :width, :source
10
+ :padding, :text, :width
13
11
 
14
12
  def self.schema_path
15
13
  ScreenKit.root_dir
16
- .join("screenkit/schemas/callouts/inline_block.json")
14
+ .join("screenkit/schemas/callout_styles/inline_block.json")
17
15
  end
18
16
 
19
- def initialize(source:, **kwargs) # rubocop:disable Lint/MissingSuper
17
+ def initialize(source:, **kwargs)
20
18
  self.class.validate!(kwargs)
21
-
22
- @source = source
19
+ super
23
20
 
24
21
  # Set default values
25
- kwargs = hi_res({
22
+ self.options = hi_res({
26
23
  text_style: {size: 50, color: "#ffffff"}.merge(text_style || {}),
27
24
  width: 600,
28
25
  padding: [10, 10, 10, 10],
29
26
  background_color: "#000000"
30
27
  }.merge(kwargs))
31
28
 
32
- kwargs.each do |key, value|
29
+ options.each do |key, value|
33
30
  value = case key
34
31
  when :padding
35
32
  Spacing.new(value)
@@ -5,20 +5,18 @@ require "mini_magick"
5
5
  module ScreenKit
6
6
  class Callout
7
7
  module Styles
8
- class Default < Base
9
- extend SchemaValidator
10
-
11
- attr_reader :background_color, :body, :body_style,
12
- :output_path, :padding, :shadow,
13
- :title, :title_style, :width, :source
8
+ class ShadowBlock < Base
9
+ attr_reader :background_color, :body, :body_style, :padding, :shadow,
10
+ :title, :title_style, :width
14
11
 
15
12
  def self.schema_path
16
- ScreenKit.root_dir.join("screenkit/schemas/callouts/default.json")
13
+ ScreenKit.root_dir
14
+ .join("screenkit/schemas/callout_styles/shadow_block.json")
17
15
  end
18
16
 
19
- def initialize(source:, **kwargs) # rubocop:disable Lint/MissingSuper
17
+ def initialize(source:, **kwargs)
20
18
  self.class.validate!(kwargs)
21
- @source = source
19
+ super
22
20
 
23
21
  # Set default values
24
22
  kwargs[:shadow] = case kwargs[:shadow]
@@ -32,9 +30,9 @@ module ScreenKit
32
30
  kwargs[:shadow]
33
31
  end
34
32
 
35
- kwargs = hi_res({width: 600}.merge(kwargs))
33
+ self.options = hi_res({width: 600}.merge(kwargs))
36
34
 
37
- kwargs.each do |key, value|
35
+ options.each do |key, value|
38
36
  value = case key
39
37
  when :body_style, :title_style
40
38
  TextStyle.new(source:, **value)
@@ -32,7 +32,7 @@ module ScreenKit
32
32
  :style_props, :style_class, :animation, :source, :log_path
33
33
 
34
34
  def self.schema_path
35
- ScreenKit.root_dir.join("screenkit/schemas/refs/callout.json")
35
+ ScreenKit.root_dir.join("screenkit/schemas/refs/callout_style.json")
36
36
  end
37
37
 
38
38
  def initialize(
@@ -35,7 +35,7 @@ module ScreenKit
35
35
  type: :string,
36
36
  required: true,
37
37
  desc: "Directory of the episode to export"
38
- option :voice_api_key,
38
+ option :tts_api_key,
39
39
  type: :string,
40
40
  desc: "API key for the voice synthesis service"
41
41
  option :overwrite,
@@ -18,6 +18,12 @@ module ScreenKit
18
18
  # The watermark configuration.
19
19
  attr_reader :watermark
20
20
 
21
+ # The demotape configuration.
22
+ attr_reader :demotape
23
+
24
+ # The callout styles configuration.
25
+ attr_reader :callout_styles
26
+
21
27
  def self.schema_path
22
28
  @schema_path ||=
23
29
  ScreenKit.root_dir.join("screenkit/schemas/episode.json")
@@ -12,8 +12,8 @@ module ScreenKit
12
12
  # The output directory for exported files.
13
13
  attr_reader :output_dir
14
14
 
15
- # Callout configurations
16
- attr_reader :callouts
15
+ # Callout styles
16
+ attr_reader :callout_styles
17
17
 
18
18
  # Scene configurations
19
19
  attr_reader :scenes
@@ -27,6 +27,9 @@ module ScreenKit
27
27
  # The watermark configuration.
28
28
  attr_reader :watermark
29
29
 
30
+ # The demotape configuration.
31
+ attr_reader :demotape
32
+
30
33
  def self.schema_path
31
34
  @schema_path ||=
32
35
  ScreenKit.root_dir.join("screenkit/schemas/project.json")
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScreenKit
4
+ Duration = DemoTape::Duration
5
+ end
@@ -5,22 +5,40 @@ module ScreenKit
5
5
  class Demotape
6
6
  include Shell
7
7
 
8
- attr_reader :demotape_path, :log_path
8
+ DURATION_ATTRIBUTES = %w[
9
+ typing_speed loop_delay run_enter_delay run_sleep
10
+ ].freeze
9
11
 
10
- def initialize(demotape_path:, log_path: nil)
12
+ attr_reader :demotape_path, :log_path, :options
13
+
14
+ def initialize(demotape_path:, options: {}, log_path: nil)
11
15
  @demotape_path = demotape_path
12
16
  @log_path = log_path
17
+ @options = options
13
18
  end
14
19
 
15
20
  def export(output_path)
16
21
  run_command "demotape",
17
22
  "run",
18
23
  demotape_path,
24
+ options_to_args(options),
25
+ "--width", 1920,
26
+ "--height", 1080,
19
27
  "--fps", 24,
20
28
  "--overwrite",
21
29
  "--output-path", output_path,
22
30
  log_path:
23
31
  end
32
+
33
+ def options_to_args(options)
34
+ (options || {}).flat_map do |key, value|
35
+ if DURATION_ATTRIBUTES.include?(key.to_s)
36
+ value = Duration.parse(value)
37
+ end
38
+
39
+ [key.to_s.tr("_", "-").prepend("--"), value]
40
+ end
41
+ end
24
42
  end
25
43
  end
26
44
  end
@@ -37,18 +37,37 @@ module ScreenKit
37
37
  @logfile = Logfile.new(output_dir.join("logs"))
38
38
  end
39
39
 
40
- def tts?
41
- config.tts || project_config.tts
40
+ def tts_available?
41
+ tts_engines.any?(&:available?)
42
42
  end
43
43
 
44
- def tts_options
45
- (config.tts || {}).merge(project_config.tts || {})
44
+ def demotape_options
45
+ (config.demotape || {}).merge(project_config.demotape || {})
46
46
  end
47
47
 
48
48
  def tts_engine
49
- @tts_engine ||= TTS.const_get(
50
- tts_options[:engine].camelize
51
- ).new(**tts_options.except(:engine))
49
+ tts_engines.find(&:available?)
50
+ end
51
+
52
+ def tts_engines
53
+ @tts_engines ||= begin
54
+ project_tts = if project_config.tts.is_a?(Hash)
55
+ [project_config.tts]
56
+ else
57
+ Array(project_config.tts)
58
+ end
59
+
60
+ episode_tts = if config.tts.is_a?(Hash)
61
+ [config.tts]
62
+ else
63
+ Array(config.tts)
64
+ end
65
+
66
+ (episode_tts + project_tts).map do |opts|
67
+ TTS.const_get(opts[:engine].camelize)
68
+ .new(**opts.except(:engine), api_key: options.tts_api_key)
69
+ end
70
+ end
52
71
  end
53
72
 
54
73
  # Logs a message to the shell with a specific category.
@@ -93,7 +112,7 @@ module ScreenKit
93
112
 
94
113
  def export_callouts
95
114
  callouts = filtered_segments.flat_map do |segment|
96
- segment.callouts.map { {prefix: segment.prefix, callouts: it} }
115
+ segment.callouts.map { {prefix: segment.prefix, callout: it} }
97
116
  end
98
117
 
99
118
  elapsed = ParallelProcessor.new(
@@ -102,15 +121,16 @@ module ScreenKit
102
121
  message: "Exporting callouts (%{progress}/%{count})"
103
122
  ).run do |item, index|
104
123
  log_path = logfile.create(item[:prefix], :callout, index)
105
- type = item[:callouts].fetch(:type).to_sym
106
- callout_style = project_config
107
- .callouts
124
+ type = item[:callout].fetch(:type).to_sym
125
+
126
+ callout_style = callout_styles
108
127
  .fetch(type)
109
- .merge(item[:callouts].except(:starts_at, :duration))
128
+ .merge(item[:callout])
110
129
  callout_path = output_dir
111
130
  .join("callouts", "#{item[:prefix]}-#{index}.png")
112
131
  Callout.new(
113
- source:, **callout_style,
132
+ source:,
133
+ **callout_style,
114
134
  output_path: callout_path,
115
135
  log_path:
116
136
  ).render
@@ -155,7 +175,9 @@ module ScreenKit
155
175
  def merge_segments
156
176
  spinner.update("Merging segments into final episode…")
157
177
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
158
- crossfade_duration = scenes.dig(:segment, :crossfade_duration) || 0.5
178
+ crossfade_duration = Duration.parse(
179
+ scenes.dig(:segment, :crossfade_duration) || 0.5
180
+ )
159
181
  watermark = Watermark.new(config.watermark || project_config.watermark)
160
182
 
161
183
  watermark_path = if watermark.path
@@ -181,7 +203,7 @@ module ScreenKit
181
203
  ]
182
204
 
183
205
  backtrack_adjustment = 1.0 / backtrack.volume
184
- backtrack_fade_volume = if tts?
206
+ backtrack_fade_volume = if tts_available?
185
207
  0.15 * backtrack_adjustment
186
208
  else
187
209
  1.0
@@ -395,7 +417,7 @@ module ScreenKit
395
417
  dir: shell.set_color(relative_path(root_dir), :blue)
396
418
  )
397
419
 
398
- unless tts?
420
+ unless tts_available?
399
421
  log(
400
422
  :info,
401
423
  shell.set_color("Voiceover is currently disabled", :red),
@@ -522,6 +544,10 @@ module ScreenKit
522
544
  end
523
545
  end
524
546
 
547
+ def callout_styles
548
+ (project_config.callout_styles || {}).merge(config.callout_styles || {})
549
+ end
550
+
525
551
  def output_video_path
526
552
  @output_video_path ||= output_dir.join("#{root_dir.basename}.mp4")
527
553
  end
@@ -86,7 +86,7 @@ module ScreenKit
86
86
  "-c:v", "libx264", "-crf", "0", "-pix_fmt", "yuv444p",
87
87
  "-c:a", "flac", "-ac", "1", "-ar", "44100",
88
88
  "-shortest",
89
- "-t", config[:duration],
89
+ "-t", Duration.parse(config[:duration]),
90
90
  "-r", 24,
91
91
  "-y",
92
92
  path
@@ -96,9 +96,9 @@ module ScreenKit
96
96
  end
97
97
 
98
98
  private def ffmpeg_params
99
- duration = config[:duration]
100
- fade_in = config.fetch(:fade_in, 0.0)
101
- fade_out = config.fetch(:fade_out, 0.5)
99
+ duration = Duration.parse(config[:duration])
100
+ fade_in = Duration.parse(config.fetch(:fade_in, 0.0))
101
+ fade_out = Duration.parse(config.fetch(:fade_out, 0.5))
102
102
  fade_out_start = duration - fade_out - 0.1
103
103
 
104
104
  # Build filter chain
@@ -71,7 +71,7 @@ module ScreenKit
71
71
  "-c:v", "libx264", "-crf", "0", "-pix_fmt", "yuv444p",
72
72
  "-c:a", "flac", "-ac", "1", "-ar", "44100",
73
73
  "-shortest",
74
- "-t", config[:duration],
74
+ "-t", Duration.parse(config[:duration]),
75
75
  "-r", 24,
76
76
  "-y",
77
77
  path
@@ -81,11 +81,11 @@ module ScreenKit
81
81
  end
82
82
 
83
83
  private def ffmpeg_params
84
- duration = config[:duration]
84
+ duration = Duration.parse(config[:duration])
85
85
  logo_delay = 0.5
86
- fade_in = config.fetch(:fade_in, 0.5)
87
- fade_out = config.fetch(:fade_out, 0.5)
88
- fade_out_start = duration - fade_out - 0.1
86
+ fade_in = Duration.parse(config.fetch(:fade_in, 0.5))
87
+ fade_out = Duration.parse(config.fetch(:fade_out, 0.5))
88
+ fade_out_start = duration - fade_out - 0.2
89
89
 
90
90
  # Build filter chain
91
91
  inputs = []