screenkit 0.0.8 → 0.0.10

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -1
  3. data/DOCUMENTATION.md +104 -1
  4. data/lib/screenkit/callout/styles/file_copy.rb +1 -2
  5. data/lib/screenkit/callout/styles/inline_block.rb +13 -5
  6. data/lib/screenkit/callout/styles/shadow_block.rb +1 -2
  7. data/lib/screenkit/callout/styles/social.rb +304 -0
  8. data/lib/screenkit/callout/text_style.rb +2 -2
  9. data/lib/screenkit/callout.rb +1 -1
  10. data/lib/screenkit/cli/episode.rb +13 -0
  11. data/lib/screenkit/config/base.rb +8 -1
  12. data/lib/screenkit/config/episode.rb +1 -1
  13. data/lib/screenkit/config/project.rb +5 -1
  14. data/lib/screenkit/exporter/episode.rb +6 -0
  15. data/lib/screenkit/exporter/intro.rb +2 -3
  16. data/lib/screenkit/exporter/outro.rb +14 -4
  17. data/lib/screenkit/exporter/segment.rb +10 -2
  18. data/lib/screenkit/exporter/video.rb +6 -4
  19. data/lib/screenkit/generators/project/.gitignore +1 -0
  20. data/lib/screenkit/generators/project/screenkit.yml +16 -0
  21. data/lib/screenkit/generators/project.rb +1 -0
  22. data/lib/screenkit/parallel_processor.rb +6 -6
  23. data/lib/screenkit/resources/callout_styles/social/blog.png +0 -0
  24. data/lib/screenkit/resources/callout_styles/social/bsky.png +0 -0
  25. data/lib/screenkit/resources/callout_styles/social/discord.png +0 -0
  26. data/lib/screenkit/resources/callout_styles/social/dribbble.png +0 -0
  27. data/lib/screenkit/resources/callout_styles/social/github.png +0 -0
  28. data/lib/screenkit/resources/callout_styles/social/instagram.png +0 -0
  29. data/lib/screenkit/resources/callout_styles/social/linkedin.png +0 -0
  30. data/lib/screenkit/resources/callout_styles/social/mastodon.png +0 -0
  31. data/lib/screenkit/resources/callout_styles/social/snap.png +0 -0
  32. data/lib/screenkit/resources/callout_styles/social/soundcloud.png +0 -0
  33. data/lib/screenkit/resources/callout_styles/social/spotify.png +0 -0
  34. data/lib/screenkit/resources/callout_styles/social/tiktok.png +0 -0
  35. data/lib/screenkit/resources/callout_styles/social/twitch.png +0 -0
  36. data/lib/screenkit/resources/callout_styles/social/youtube.png +0 -0
  37. data/lib/screenkit/schemas/callout_styles/social.json +55 -0
  38. data/lib/screenkit/schemas/refs/text_style.json +4 -0
  39. data/lib/screenkit/sound.rb +1 -1
  40. data/lib/screenkit/tts/espeak.rb +1 -1
  41. data/lib/screenkit/tts/say.rb +1 -1
  42. data/lib/screenkit/version.rb +1 -1
  43. data/lib/screenkit.rb +6 -1
  44. data/screenkit.gemspec +1 -0
  45. metadata +36 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9432b0beeacae6f145c48e9935c18d92c62c08c69ea7177a3d282db689b0d9d5
4
- data.tar.gz: 9cbf970a8998f0cb3bd5778489faa0ecc521b800862c1d8c8112cb11e0232bcd
3
+ metadata.gz: 8e33c3c018a611a86e9ece01bf1cac931c8df89868c515eeaf8208c11888c946
4
+ data.tar.gz: 6b8c37eb2dc866aec21660cd4bb58e5641f67e6eff27cc1e5d44304c590bf699
5
5
  SHA512:
6
- metadata.gz: 4b952dc0d719ccdf1496b2ee0cb430e9ba8271e5e9373f1859d8132ed68febf86daa9ff063cbc05a75a314de32d8a279eb4336021344caf70e9cc297e8eaac37
7
- data.tar.gz: 4660532603407564ab3315a0d21d6af592d5fad3179cbd7bafade4438e97708a95ba2d9eab34fd61507aee02b58c90bed5d487f7affa4951351a1d3442c37d46
6
+ metadata.gz: 3329f278b4fce62ca42bb9d9fec7ad8ec93b62fae9f0fa949a9444e9be45b68ddf755ea6c5b536176c2031d2a9ae24fc8ed0e99c4fa8918377a319af14772c1f
7
+ data.tar.gz: 8e6a1089e6b396874e60d58b84140726b45b78532cddb54871eda524fd11382e383aebd4eed00a371d14d46b9db524854a89981619eb8bf066fe1f9aadeedc6a
data/CHANGELOG.md CHANGED
@@ -11,7 +11,17 @@ Prefix your message with one of the following:
11
11
  - [Security] in case of vulnerabilities.
12
12
  -->
13
13
 
14
- # v0.0.8
14
+ ## v0.0.10
15
+
16
+ - [Added] Add `--overwrite-content` and `--overwrite-voiceover` to individually
17
+ control overwriting content and voiceover when generating episodes.
18
+
19
+ ## v0.0.9
20
+
21
+ - [Added] Add social callouts.
22
+ - [Added] Add align support for inline block callouts.
23
+
24
+ ## v0.0.8
15
25
 
16
26
  - [Fixed] Further improvements to support extensions.
17
27
 
data/DOCUMENTATION.md CHANGED
@@ -474,7 +474,109 @@ during the video.
474
474
 
475
475
  ### Callout Styles
476
476
 
477
- ScreenKit provides two built-in callout styles:
477
+ ScreenKit provides a few built-in callout styles:
478
+
479
+ #### Social Style
480
+
481
+ The social callout style displays social media information with icons.
482
+
483
+ ```yaml
484
+ callout_styles:
485
+ # Basic setup to use presets
486
+ social:
487
+ style: social
488
+ anchor: [left, bottom]
489
+ margin: 100
490
+ animation: fade
491
+ in_transition:
492
+ duration: 0.4s
493
+ sound: pop.mp3
494
+ out_transition:
495
+ duration: 0.4s
496
+ sound: pop.mp3
497
+
498
+ # Custom setup
499
+ custom_social:
500
+ style: social
501
+ background_color: "#E0D300"
502
+ label_style:
503
+ size: 24
504
+ color: "#00000088"
505
+ font_path: "open-sans/OpenSans-ExtraBold.ttf"
506
+ text_style:
507
+ size: 50
508
+ color: "#000000"
509
+ font_path: "open-sans/OpenSans-ExtraBold.ttf"
510
+ label: "CUSTOM"
511
+ icon:
512
+ path: "images/custom.png" # Must be a 160x160 image
513
+ background_color: "#000000"
514
+ anchor: [left, bottom]
515
+ margin: 100
516
+ animation: fade
517
+ in_transition:
518
+ duration: 0.4s
519
+ sound: pop.mp3
520
+ out_transition:
521
+ duration: 0.4s
522
+ sound: pop.mp3
523
+ ```
524
+
525
+ ##### Usage in episode
526
+
527
+ ScreenKit comes with some presets where you don't need to set up anything, but
528
+ you can also create custom callouts.
529
+
530
+ Available presets:
531
+
532
+ - `blog`
533
+ - `bsky`
534
+ - `discord`
535
+ - `dribbble`
536
+ - `github`
537
+ - `linkedin`
538
+ - `mastodon`
539
+ - `snap`
540
+ - `soundcloud`
541
+ - `spotify`
542
+ - `tiktok`
543
+ - `twitch`
544
+ - `youtube`
545
+
546
+ ```yaml
547
+ callouts:
548
+ # Using a preset
549
+ - type: social
550
+ preset: github
551
+ text: "@fnando"
552
+ starts: 1
553
+ duration: 5s
554
+
555
+ # Using a custom callout style
556
+ - type: custom_social
557
+ text: "@fnando"
558
+ starts: 1
559
+ duration: 5s
560
+
561
+ # Defining a custom callout inline
562
+ - type: social
563
+ text: "@fnando"
564
+ starts: 1
565
+ duration: 5s
566
+ background_color: "#E0D300"
567
+ label_style:
568
+ size: 24
569
+ color: "#00000088"
570
+ font_path: "open-sans/OpenSans-ExtraBold.ttf"
571
+ text_style:
572
+ size: 50
573
+ color: "#000000"
574
+ font_path: "open-sans/OpenSans-ExtraBold.ttf"
575
+ label: "CUSTOM"
576
+ icon:
577
+ path: "images/custom.png" # Must be a 160x160 image
578
+ background_color: "#000000"
579
+ ```
478
580
 
479
581
  #### Shadow Block Style
480
582
 
@@ -552,6 +654,7 @@ callouts_styles:
552
654
  color: "#ffffff"
553
655
  size: 40
554
656
  font_path: open-sans/OpenSans-ExtraBold.ttf
657
+ align: left # Text alignment: left, center, right
555
658
 
556
659
  # Layout
557
660
  padding: 20
@@ -5,8 +5,7 @@ module ScreenKit
5
5
  module Styles
6
6
  class FileCopy < Base
7
7
  def self.schema_path
8
- ScreenKit.root_dir
9
- .join("screenkit/schemas/callout_styles/file_copy.json")
8
+ ScreenKit.root_dir.join("schemas/callout_styles/file_copy.json")
10
9
  end
11
10
 
12
11
  def initialize(source:, **kwargs)
@@ -10,8 +10,7 @@ module ScreenKit
10
10
  :padding, :text, :width
11
11
 
12
12
  def self.schema_path
13
- ScreenKit.root_dir
14
- .join("screenkit/schemas/callout_styles/inline_block.json")
13
+ ScreenKit.root_dir.join("schemas/callout_styles/inline_block.json")
15
14
  end
16
15
 
17
16
  def initialize(source:, **kwargs)
@@ -88,18 +87,27 @@ module ScreenKit
88
87
  image << "xc:none"
89
88
 
90
89
  line_images.each do |path, width, height|
90
+ offset_x = case text_style.align
91
+ when "right"
92
+ max_line_width - width
93
+ when "center"
94
+ (max_line_width - width) / 2
95
+ else
96
+ 0
97
+ end
98
+
91
99
  # Draw rectangle background
92
100
  image << "-fill"
93
101
  image << background_color
94
102
  image << "-draw"
95
- image << "rectangle 0,#{offset_y}," \
96
- "#{width + padding_x}," \
103
+ image << "rectangle #{offset_x},#{offset_y}," \
104
+ "#{width + padding_x + offset_x}," \
97
105
  "#{offset_y + height + padding_y}"
98
106
 
99
107
  # Composite line text
100
108
  image << path
101
109
  image << "-geometry"
102
- image << "+#{padding.top}+#{offset_y + padding.left}"
110
+ image << "+#{padding.left + offset_x}+#{offset_y + padding.top}"
103
111
  image << "-composite"
104
112
  offset_y += padding_y + height
105
113
  end
@@ -10,8 +10,7 @@ module ScreenKit
10
10
  :title, :title_style, :width
11
11
 
12
12
  def self.schema_path
13
- ScreenKit.root_dir
14
- .join("screenkit/schemas/callout_styles/shadow_block.json")
13
+ ScreenKit.root_dir.join("schemas/callout_styles/shadow_block.json")
15
14
  end
16
15
 
17
16
  def initialize(source:, **kwargs)
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_magick"
4
+
5
+ module ScreenKit
6
+ class Callout
7
+ module Styles
8
+ class Social < Base
9
+ attr_reader :text, :label, :icon, :label_style, :text_style,
10
+ :background_color
11
+
12
+ LABEL_STYLE = {
13
+ color: "#ffffff88",
14
+ size: 24,
15
+ font_path: "open-sans/OpenSans-ExtraBold.ttf"
16
+ }.freeze
17
+
18
+ TEXT_STYLE = {
19
+ color: "#ffffff",
20
+ size: 50,
21
+ font_path: "open-sans/OpenSans-ExtraBold.ttf"
22
+ }.freeze
23
+
24
+ def self.presets
25
+ @presets ||= {
26
+ instagram: {
27
+ label: "INSTAGRAM",
28
+ icon: {
29
+ path: "callout_styles/social/instagram.png",
30
+ background_color: "#C13684"
31
+ },
32
+ label_style: LABEL_STYLE,
33
+ text_style: TEXT_STYLE,
34
+ background_color: "#693050"
35
+ },
36
+
37
+ spotify: {
38
+ label: "SPOTIFY",
39
+ icon: {
40
+ path: "callout_styles/social/spotify.png",
41
+ background_color: "#1cd760"
42
+ },
43
+ label_style: LABEL_STYLE,
44
+ text_style: TEXT_STYLE,
45
+ background_color: "#205C36"
46
+ },
47
+
48
+ bsky: {
49
+ label: "BSKY",
50
+ icon: {
51
+ path: "callout_styles/social/bsky.png",
52
+ background_color: "#0085FF"
53
+ },
54
+ label_style: LABEL_STYLE,
55
+ text_style: TEXT_STYLE,
56
+ background_color: "#004483"
57
+ },
58
+
59
+ linkedin: {
60
+ label: "LINKEDIN",
61
+ icon: {
62
+ path: "callout_styles/social/linkedin.png",
63
+ background_color: "#007EBB"
64
+ },
65
+ label_style: LABEL_STYLE,
66
+ text_style: TEXT_STYLE,
67
+ background_color: "#00486A"
68
+ },
69
+
70
+ dribbble: {
71
+ label: "DRIBBBLE",
72
+ icon: {
73
+ path: "callout_styles/social/dribbble.png",
74
+ background_color: "#EC4989"
75
+ },
76
+ label_style: LABEL_STYLE,
77
+ text_style: TEXT_STYLE,
78
+ background_color: "#7A2245"
79
+ },
80
+
81
+ tiktok: {
82
+ label: "TIKTOK",
83
+ icon: {
84
+ path: "callout_styles/social/tiktok.png",
85
+ background_color: "#000000"
86
+ },
87
+ label_style: LABEL_STYLE,
88
+ text_style: TEXT_STYLE,
89
+ background_color: "#252525"
90
+ },
91
+
92
+ youtube: {
93
+ label: "YOUTUBE",
94
+ icon: {
95
+ path: "callout_styles/social/youtube.png",
96
+ background_color: "#FF0000"
97
+ },
98
+ label_style: LABEL_STYLE,
99
+ text_style: TEXT_STYLE,
100
+ background_color: "#7B1212"
101
+ },
102
+
103
+ snap: {
104
+ label: "SNAP",
105
+ icon: {
106
+ path: "callout_styles/social/snap.png",
107
+ background_color: "#FEFC05"
108
+ },
109
+ label_style: LABEL_STYLE,
110
+ text_style: TEXT_STYLE,
111
+ background_color: "#51511A"
112
+ },
113
+
114
+ mastodon: {
115
+ label: "MASTODON",
116
+ icon: {
117
+ path: "callout_styles/social/mastodon.png",
118
+ background_color: "#3188D4"
119
+ },
120
+ label_style: LABEL_STYLE,
121
+ text_style: TEXT_STYLE,
122
+ background_color: "#1F4566"
123
+ },
124
+
125
+ blog: {
126
+ label: "BLOG",
127
+ icon: {
128
+ path: "callout_styles/social/blog.png",
129
+ background_color: "#5831D4"
130
+ },
131
+ label_style: LABEL_STYLE,
132
+ text_style: TEXT_STYLE,
133
+ background_color: "#321E6F"
134
+ },
135
+
136
+ twitch: {
137
+ label: "TWITCH",
138
+ icon: {
139
+ path: "callout_styles/social/twitch.png",
140
+ background_color: "#9146FF"
141
+ },
142
+ label_style: LABEL_STYLE,
143
+ text_style: TEXT_STYLE,
144
+ background_color: "#461B85"
145
+ },
146
+
147
+ github: {
148
+ label: "GITHUB",
149
+ icon: {
150
+ path: "callout_styles/social/github.png",
151
+ background_color: "#161514"
152
+ },
153
+ label_style: LABEL_STYLE,
154
+ text_style: TEXT_STYLE,
155
+ background_color: "#373737"
156
+ },
157
+
158
+ soundcloud: {
159
+ label: "SOUNDCLOUD",
160
+ icon: {
161
+ path: "callout_styles/social/soundcloud.png",
162
+ background_color: "#FE2401"
163
+ },
164
+ label_style: LABEL_STYLE,
165
+ text_style: TEXT_STYLE,
166
+ background_color: "#6D1507"
167
+ },
168
+
169
+ discord: {
170
+ label: "DISCORD",
171
+ icon: {
172
+ path: "callout_styles/social/discord.png",
173
+ background_color: "#5865F2"
174
+ },
175
+ label_style: LABEL_STYLE,
176
+ text_style: TEXT_STYLE,
177
+ background_color: "#2E389D"
178
+ }
179
+ }
180
+ end
181
+
182
+ def self.schema_path
183
+ ScreenKit.root_dir.join("schemas/callout_styles/social.json")
184
+ end
185
+
186
+ def initialize(source:, text:, preset: nil, **kwargs)
187
+ @text = text
188
+ self.class.validate!(kwargs)
189
+ super
190
+ self.options = self.class.presets.fetch(preset.to_sym) if preset
191
+
192
+ options.each do |key, value|
193
+ value = case key
194
+ when :label_style, :text_style
195
+ TextStyle.new(source:, **hi_res(**value))
196
+ else
197
+ value
198
+ end
199
+
200
+ instance_variable_set(:"@#{key}", value)
201
+ end
202
+ end
203
+
204
+ def as_json(*)
205
+ {}
206
+ end
207
+
208
+ def render
209
+ label_path, label_width, label_height = render_text_image(
210
+ type: "label",
211
+ text: label,
212
+ style: label_style,
213
+ width: 600
214
+ )
215
+
216
+ text_path, text_width, _ = render_text_image(
217
+ type: "label",
218
+ text:,
219
+ style: text_style,
220
+ width: 600
221
+ )
222
+
223
+ sizes = hi_res(
224
+ icon: 132,
225
+ padding: 20,
226
+ content_width: [label_width, text_width].max,
227
+ image_height: 172,
228
+ icon_radius: 30,
229
+ panel_radius: 40
230
+ )
231
+
232
+ image_width = (sizes[:icon] + (sizes[:padding] * 2)) +
233
+ sizes[:padding] +
234
+ (sizes[:content_width] / 2) +
235
+ (sizes[:padding] * 2)
236
+ image_height = sizes[:image_height]
237
+ offset_x = sizes[:icon] + sizes[:padding]
238
+ offset_y = sizes[:padding]
239
+ icon_y = offset_y + sizes[:icon]
240
+
241
+ MiniMagick.convert do |image|
242
+ # Create transparent canvas
243
+ image << "-size"
244
+ image << "#{image_width}x#{image_height}"
245
+ image << "xc:none"
246
+
247
+ # Draw main background
248
+ image << "-fill"
249
+ image << options[:background_color]
250
+ image << "-draw"
251
+ image << "roundrectangle 0,0,#{image_width},#{image_height}," \
252
+ "#{sizes[:panel_radius]},#{sizes[:panel_radius]}"
253
+
254
+ # Draw icon background
255
+ image << "-fill"
256
+ image << icon[:background_color]
257
+ image << "-draw"
258
+ image << "roundrectangle #{sizes[:padding]},#{sizes[:padding]}," \
259
+ "#{offset_x},#{icon_y}," \
260
+ "#{sizes[:icon_radius]},#{sizes[:icon_radius]}"
261
+
262
+ # Draw icon
263
+ icon_path = source.search(icon[:path])
264
+ icon_image = MiniMagick::Image.open(icon_path)
265
+ icon_x = sizes[:padding] + ((sizes[:icon] - icon_image.width) / 2)
266
+ icon_y = offset_y + ((sizes[:icon] - icon_image.height) / 2)
267
+
268
+ image << icon_path
269
+ image << "-geometry"
270
+ image << "+#{icon_x}+#{icon_y}"
271
+ image << "-composite"
272
+
273
+ offset_x += sizes[:padding]
274
+ offset_y += sizes[:padding] / 2
275
+
276
+ # Composite label
277
+ image << label_path
278
+ image << "-geometry"
279
+ image << "+#{offset_x}+#{offset_y}"
280
+ image << "-composite"
281
+
282
+ offset_y += label_height
283
+
284
+ # Composite text
285
+ image << text_path
286
+ image << "-geometry"
287
+ image << "+#{offset_x}+#{offset_y}"
288
+ image << "-composite"
289
+
290
+ image << "PNG:#{output_path}"
291
+ end
292
+
293
+ output_path
294
+ rescue MiniMagick::Error => error
295
+ retry if error.message.include?("No such file or directory")
296
+ raise
297
+ ensure
298
+ remove_file(label_path)
299
+ remove_file(text_path)
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -3,7 +3,7 @@
3
3
  module ScreenKit
4
4
  class Callout
5
5
  class TextStyle
6
- attr_reader :color, :size, :font_path
6
+ attr_reader :color, :size, :font_path, :align
7
7
 
8
8
  def initialize(source:, **kwargs)
9
9
  @source = source
@@ -37,7 +37,7 @@ module ScreenKit
37
37
  end
38
38
 
39
39
  def as_json(*)
40
- {color:, size:, font_path:, rgb_color:, opacity:}
40
+ {color:, size:, font_path:, rgb_color:, opacity:, align:}
41
41
  end
42
42
  end
43
43
  end
@@ -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_style.json")
35
+ ScreenKit.root_dir.join("schemas/refs/callout_style.json")
36
36
  end
37
37
 
38
38
  def initialize(
@@ -41,6 +41,14 @@ module ScreenKit
41
41
  type: :boolean,
42
42
  default: false,
43
43
  desc: "Overwrite existing exported file"
44
+ option :overwrite_voiceover,
45
+ type: :boolean,
46
+ default: false,
47
+ desc: "Regenerate all voiceover audio files"
48
+ option :overwrite_content,
49
+ type: :boolean,
50
+ default: false,
51
+ desc: "Regenerate all content files (e.g., demo tapes)"
44
52
  option :match_segment,
45
53
  type: :string,
46
54
  desc: "Only export segments matching this string"
@@ -71,6 +79,11 @@ module ScreenKit
71
79
 
72
80
  options.require.each { require(it) }
73
81
 
82
+ # Ensure overwrite is only set if other individual options aren't set.
83
+ options[:overwrite] = options.overwrite &&
84
+ !options.overwrite_content &&
85
+ !options.overwrite_voiceover
86
+
74
87
  exporter = ScreenKit::Exporter::Episode.new(
75
88
  project_config: config,
76
89
  config: episode_config,
@@ -5,12 +5,17 @@ module ScreenKit
5
5
  class Base
6
6
  extend SchemaValidator
7
7
 
8
+ attr_reader :raw_options
9
+
8
10
  def self.load_file(path)
9
11
  unless File.file?(path)
10
12
  raise FileNotFoundError, "Config file not found: #{path}"
11
13
  end
12
14
 
13
- config = YAML.load_file(path, symbolize_names: true)
15
+ template = File.read(path)
16
+ contents = ERB.new(template).result
17
+
18
+ config = YAML.load(contents, symbolize_names: true)
14
19
  load(config)
15
20
  end
16
21
 
@@ -21,6 +26,8 @@ module ScreenKit
21
26
  end
22
27
 
23
28
  def initialize(**kwargs)
29
+ @raw_options = kwargs
30
+
24
31
  kwargs.each do |key, value|
25
32
  value = process(key, value)
26
33
  instance_variable_set(:"@#{key}", value)
@@ -26,7 +26,7 @@ module ScreenKit
26
26
 
27
27
  def self.schema_path
28
28
  @schema_path ||=
29
- ScreenKit.root_dir.join("screenkit/schemas/episode.json")
29
+ ScreenKit.root_dir.join("schemas/episode.json")
30
30
  end
31
31
 
32
32
  def initialize(**)
@@ -32,7 +32,7 @@ module ScreenKit
32
32
 
33
33
  def self.schema_path
34
34
  @schema_path ||=
35
- ScreenKit.root_dir.join("screenkit/schemas/project.json")
35
+ ScreenKit.root_dir.join("schemas/project.json")
36
36
  end
37
37
 
38
38
  private def process(key, value)
@@ -45,6 +45,10 @@ module ScreenKit
45
45
  value
46
46
  end
47
47
  end
48
+
49
+ def to_h
50
+ raw_options
51
+ end
48
52
  end
49
53
  end
50
54
  end
@@ -35,6 +35,8 @@ module ScreenKit
35
35
  @options = options
36
36
  @mutex = Mutex.new
37
37
  @logfile = Logfile.new(output_dir.join("logs"))
38
+
39
+ Thread.report_on_exception = false
38
40
  end
39
41
 
40
42
  def tts_available?
@@ -106,6 +108,10 @@ module ScreenKit
106
108
  def export
107
109
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
108
110
 
111
+ Signal.trap("INT", "EXIT") do
112
+ exit 1
113
+ end
114
+
109
115
  cleanup_output_dir
110
116
  prelude
111
117
  create_output_dir
@@ -8,7 +8,7 @@ module ScreenKit
8
8
  extend SchemaValidator
9
9
 
10
10
  def self.schema_path
11
- ScreenKit.root_dir.join("screenkit/schemas/refs/intro.json")
11
+ ScreenKit.root_dir.join("schemas/refs/intro.json")
12
12
  end
13
13
 
14
14
  # The intro scene configuration.
@@ -109,11 +109,10 @@ module ScreenKit
109
109
  # Background layer
110
110
  if background_path&.file?
111
111
  if video_file?(background_path)
112
- # Ensure video is 24fps
113
112
  extname = background_path.extname
114
113
  optimized_path = background_path.sub_ext("_24fps#{extname}")
115
114
 
116
- if (-0.02..0.02).cover?(fps(background_path))
115
+ if Video.right_fps?(background_path)
117
116
  optimized_path = background_path
118
117
  end
119
118
 
@@ -8,7 +8,7 @@ module ScreenKit
8
8
  extend SchemaValidator
9
9
 
10
10
  def self.schema_path
11
- ScreenKit.root_dir.join("screenkit/schemas/refs/outro.json")
11
+ ScreenKit.root_dir.join("schemas/refs/outro.json")
12
12
  end
13
13
 
14
14
  # The outro scene configuration.
@@ -65,7 +65,6 @@ module ScreenKit
65
65
  cmd = [
66
66
  "ffmpeg",
67
67
  *inputs,
68
- "-sws_flags", "lanczos+accurate_rnd+full_chroma_int",
69
68
  "-filter_complex", filters,
70
69
  *maps,
71
70
  "-c:v", "libx264", "-crf", "0", "-pix_fmt", "yuv444p",
@@ -95,15 +94,26 @@ module ScreenKit
95
94
  # Background layer
96
95
  if background_path&.file?
97
96
  if video_file?(background_path)
97
+ extname = background_path.extname
98
+ optimized_path = background_path.sub_ext("_24fps#{extname}")
99
+
100
+ if Video.right_fps?(background_path)
101
+ optimized_path = background_path
102
+ end
103
+
104
+ unless optimized_path.file?
105
+ Video.new(input_path: background_path).export(optimized_path)
106
+ end
107
+
98
108
  # Video background
99
- video_duration = duration(background_path)
109
+ video_duration = duration(optimized_path)
100
110
 
101
111
  # Calculate how many loops we need
102
112
  loops_needed = (duration / video_duration).ceil
103
113
 
104
114
  inputs += [
105
115
  "-stream_loop", (loops_needed - 1).to_s, "-i",
106
- background_path
116
+ optimized_path
107
117
  ]
108
118
 
109
119
  # Scale, crop, then trim to exact duration needed
@@ -60,7 +60,7 @@ module ScreenKit
60
60
  end
61
61
 
62
62
  def export_video(log_path:)
63
- return if video_path.file? && !episode.options.overwrite
63
+ return if video_path.file? && !overwrite_content?
64
64
 
65
65
  log_path = format(log_path.to_s, prefix:) if log_path
66
66
 
@@ -181,8 +181,16 @@ module ScreenKit
181
181
  @script_content ||= script_path.read if script_path.file?
182
182
  end
183
183
 
184
+ def overwrite_voiceover?
185
+ episode.options.overwrite || episode.options.overwrite_voiceover
186
+ end
187
+
188
+ def overwrite_content?
189
+ episode.options.overwrite || episode.options.overwrite_content
190
+ end
191
+
184
192
  def create_voiceover(log_path:)
185
- return if voiceover_path&.file? && !episode.options.overwrite
193
+ return if voiceover_path&.file? && !overwrite_voiceover?
186
194
  return unless script_path.file?
187
195
  return unless episode.tts_available?
188
196
 
@@ -4,7 +4,7 @@ module ScreenKit
4
4
  module Exporter
5
5
  class Video
6
6
  include Shell
7
- include Utils
7
+ extend Utils
8
8
 
9
9
  # The path to the input video file.
10
10
  attr_reader :input_path
@@ -17,10 +17,12 @@ module ScreenKit
17
17
  @log_path = log_path
18
18
  end
19
19
 
20
- def export(output_path)
21
- fps = fps(input_path)
20
+ def self.right_fps?(path)
21
+ (-0.02..0.02).cover?(24 - fps(path))
22
+ end
22
23
 
23
- if (-0.02..0.02).cover?(24 - fps)
24
+ def export(output_path)
25
+ if self.class.right_fps?(input_path)
24
26
  FileUtils.cp(input_path, output_path)
25
27
  return
26
28
  end
@@ -0,0 +1 @@
1
+ /output
@@ -41,6 +41,7 @@ resources_dir:
41
41
  - "%{episode_dir}/resources/fonts"
42
42
  - resources
43
43
  - "%{episode_dir}/resources"
44
+ - "<%= ScreenKit.resources_dir %>"
44
45
  - ~/Library/Fonts
45
46
  - /Library/Fonts
46
47
  - /System/Library/Fonts
@@ -272,6 +273,8 @@ demotape:
272
273
  # in the video. Callouts are visual elements that highlight important
273
274
  # information. Each callout type can have its own settings for icon, background,
274
275
  # text styles, animations, and sounds.
276
+ #
277
+ # https://github.com/fnando/screenkit/blob/main/DOCUMENTATION.md#callouts
275
278
  callout_styles:
276
279
  shadow_block:
277
280
  # yaml-language-server: $schema=https://screenkit.dev/schemas/callout_styles/default.json
@@ -311,3 +314,16 @@ callout_styles:
311
314
  out_transition:
312
315
  duration: 0.4s
313
316
  sound: pop.mp3
317
+
318
+ social:
319
+ # yaml-language-server: $schema=https://screenkit.dev/schemas/callout_styles/social.json
320
+ style: social
321
+ anchor: [left, bottom]
322
+ margin: 100
323
+ animation: fade
324
+ in_transition:
325
+ duration: 0.4s
326
+ sound: pop.mp3
327
+ out_transition:
328
+ duration: 0.4s
329
+ sound: pop.mp3
@@ -21,6 +21,7 @@ module ScreenKit
21
21
 
22
22
  def copy_files
23
23
  copy_file "screenkit.yml"
24
+ copy_file ".gitignore"
24
25
  directory "resources", exclude_pattern: /DS_Store/
25
26
  directory ".github", exclude_pattern: /DS_Store/
26
27
  end
@@ -23,11 +23,14 @@ module ScreenKit
23
23
  indexed_list.each_slice(Etc.nprocessors) do |slice|
24
24
  threads = slice.map do |args|
25
25
  thread = Thread.new do
26
+ update_message
26
27
  yield(*args.take([1, arity].max), log_path:)
27
- update_progress
28
+ mutex.synchronize { self.progress += 1 }
29
+ update_message
28
30
  end
29
31
 
30
32
  thread.abort_on_exception = true
33
+ thread.report_on_exception = false
31
34
  thread
32
35
  end
33
36
 
@@ -40,11 +43,8 @@ module ScreenKit
40
43
  ended_at - started_at
41
44
  end
42
45
 
43
- def update_progress
44
- mutex.synchronize do
45
- self.progress += 1
46
- spinner.update(format(message, count:, progress:))
47
- end
46
+ def update_message
47
+ spinner.update(format(message, count:, progress:))
48
48
  end
49
49
  end
50
50
  end
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "$id": "https://screenkit.dev/schemas/callout_styles/shadow_block.json",
4
+ "title": "Social Callout",
5
+ "oneOf": [
6
+ {
7
+ "preset": {
8
+ "type": "string",
9
+ "enum": [
10
+ "spotify",
11
+ "youtube",
12
+ "tiktok",
13
+ "snap",
14
+ "mastodon",
15
+ "blog",
16
+ "linkedin",
17
+ "discord",
18
+ "twitch",
19
+ "github",
20
+ "soundcloud",
21
+ "dribbble"
22
+ ]
23
+ }
24
+ },
25
+ {
26
+ "type": "object",
27
+ "required": ["label", "icon", "label_style", "account_style"],
28
+ "properties": {
29
+ "icon": {
30
+ "type": "object",
31
+ "required": ["path", "background_color"],
32
+ "properties": {
33
+ "path": { "type": "string", "format": "uri" },
34
+ "background_color": { "$ref": "../refs/color.json" }
35
+ }
36
+ },
37
+ "background_color": {
38
+ "$ref": "../refs/color.json"
39
+ },
40
+ "label_style": {
41
+ "$ref": "../refs/text_style.json"
42
+ },
43
+ "account_style": {
44
+ "$ref": "../refs/text_style.json"
45
+ },
46
+ "label": {
47
+ "type": "string"
48
+ },
49
+ "text": {
50
+ "type": "string"
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ }
@@ -13,6 +13,10 @@
13
13
  },
14
14
  "color": {
15
15
  "$ref": "color.json"
16
+ },
17
+ "align": {
18
+ "type": "string",
19
+ "enum": ["left", "center", "right"]
16
20
  }
17
21
  }
18
22
  }
@@ -20,7 +20,7 @@ module ScreenKit
20
20
 
21
21
  case input
22
22
  when FalseClass, nil
23
- @path = ScreenKit.root_dir.join("screenkit/resources/mute.mp3")
23
+ @path = ScreenKit.resources_dir.join("mute.mp3")
24
24
  when Hash
25
25
  {path: nil, volume: 1.0}.merge(input) => {path:, volume:}
26
26
  @path = Pathname(path)
@@ -10,7 +10,7 @@ module ScreenKit
10
10
  end
11
11
 
12
12
  def self.schema_path
13
- ScreenKit.root_dir.join("screenkit/schemas/tts/espeak.json")
13
+ ScreenKit.root_dir.join("schemas/tts/espeak.json")
14
14
  end
15
15
 
16
16
  def generate(text:, output_path:, log_path: nil)
@@ -6,7 +6,7 @@ module ScreenKit
6
6
  extend Shell
7
7
 
8
8
  def self.schema_path
9
- ScreenKit.root_dir.join("screenkit/schemas/tts/say.json")
9
+ ScreenKit.root_dir.join("schemas/tts/say.json")
10
10
  end
11
11
 
12
12
  def self.available?(**)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScreenKit
4
- VERSION = "0.0.8"
4
+ VERSION = "0.0.10"
5
5
  end
data/lib/screenkit.rb CHANGED
@@ -11,6 +11,7 @@ require "etc"
11
11
  require "securerandom"
12
12
  require "demo_tape/duration"
13
13
  require "aitch"
14
+ require "erb"
14
15
 
15
16
  module ScreenKit
16
17
  require_relative "screenkit/version"
@@ -67,7 +68,11 @@ module ScreenKit
67
68
  require_files.call("screenkit/tts/*.rb")
68
69
 
69
70
  def self.root_dir
70
- @root_dir ||= Pathname(__dir__)
71
+ @root_dir ||= Pathname(__dir__).join("screenkit")
72
+ end
73
+
74
+ def self.resources_dir
75
+ @resources_dir ||= root_dir.join("resources")
71
76
  end
72
77
 
73
78
  # Raised when the configuration schema is invalid.
data/screenkit.gemspec CHANGED
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
37
37
 
38
38
  spec.add_dependency "aitch"
39
39
  spec.add_dependency "demotape"
40
+ spec.add_dependency "erb"
40
41
  spec.add_dependency "etc"
41
42
  spec.add_dependency "json-schema"
42
43
  spec.add_dependency "mini_magick"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: screenkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Vieira
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: erb
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: etc
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -289,6 +303,7 @@ files:
289
303
  - lib/screenkit/callout/styles/file_copy.rb
290
304
  - lib/screenkit/callout/styles/inline_block.rb
291
305
  - lib/screenkit/callout/styles/shadow_block.rb
306
+ - lib/screenkit/callout/styles/social.rb
292
307
  - lib/screenkit/callout/text_style.rb
293
308
  - lib/screenkit/cli.rb
294
309
  - lib/screenkit/cli/base.rb
@@ -316,6 +331,7 @@ files:
316
331
  - lib/screenkit/generators/episode/scripts/001.txt
317
332
  - lib/screenkit/generators/project.rb
318
333
  - lib/screenkit/generators/project/.github/screenkit.yml
334
+ - lib/screenkit/generators/project/.gitignore
319
335
  - lib/screenkit/generators/project/Gemfile.erb
320
336
  - lib/screenkit/generators/project/resources/backtracks/default.aac
321
337
  - lib/screenkit/generators/project/resources/fonts/open-sans/OFL.txt
@@ -332,12 +348,27 @@ files:
332
348
  - lib/screenkit/logfile.rb
333
349
  - lib/screenkit/parallel_processor.rb
334
350
  - lib/screenkit/path_lookup.rb
351
+ - lib/screenkit/resources/callout_styles/social/blog.png
352
+ - lib/screenkit/resources/callout_styles/social/bsky.png
353
+ - lib/screenkit/resources/callout_styles/social/discord.png
354
+ - lib/screenkit/resources/callout_styles/social/dribbble.png
355
+ - lib/screenkit/resources/callout_styles/social/github.png
356
+ - lib/screenkit/resources/callout_styles/social/instagram.png
357
+ - lib/screenkit/resources/callout_styles/social/linkedin.png
358
+ - lib/screenkit/resources/callout_styles/social/mastodon.png
359
+ - lib/screenkit/resources/callout_styles/social/snap.png
360
+ - lib/screenkit/resources/callout_styles/social/soundcloud.png
361
+ - lib/screenkit/resources/callout_styles/social/spotify.png
362
+ - lib/screenkit/resources/callout_styles/social/tiktok.png
363
+ - lib/screenkit/resources/callout_styles/social/twitch.png
364
+ - lib/screenkit/resources/callout_styles/social/youtube.png
335
365
  - lib/screenkit/resources/mute.mp3
336
366
  - lib/screenkit/resources/transparent.png
337
367
  - lib/screenkit/schema_validator.rb
338
368
  - lib/screenkit/schemas/callout_styles/file_copy.json
339
369
  - lib/screenkit/schemas/callout_styles/inline_block.json
340
370
  - lib/screenkit/schemas/callout_styles/shadow_block.json
371
+ - lib/screenkit/schemas/callout_styles/social.json
341
372
  - lib/screenkit/schemas/callouts/inline_block.json
342
373
  - lib/screenkit/schemas/callouts/shadow_block.json
343
374
  - lib/screenkit/schemas/episode.json
@@ -388,10 +419,10 @@ metadata:
388
419
  rubygems_mfa_required: 'true'
389
420
  homepage_uri: https://github.com/fnando/screenkit
390
421
  bug_tracker_uri: https://github.com/fnando/screenkit/issues
391
- source_code_uri: https://github.com/fnando/screenkit/tree/v0.0.8
392
- changelog_uri: https://github.com/fnando/screenkit/tree/v0.0.8/CHANGELOG.md
393
- documentation_uri: https://github.com/fnando/screenkit/tree/v0.0.8/README.md
394
- license_uri: https://github.com/fnando/screenkit/tree/v0.0.8/LICENSE.md
422
+ source_code_uri: https://github.com/fnando/screenkit/tree/v0.0.10
423
+ changelog_uri: https://github.com/fnando/screenkit/tree/v0.0.10/CHANGELOG.md
424
+ documentation_uri: https://github.com/fnando/screenkit/tree/v0.0.10/README.md
425
+ license_uri: https://github.com/fnando/screenkit/tree/v0.0.10/LICENSE.md
395
426
  rdoc_options: []
396
427
  require_paths:
397
428
  - lib