screenkit 0.0.7 → 0.0.9

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/DOCUMENTATION.md +131 -12
  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/config/base.rb +8 -1
  11. data/lib/screenkit/config/episode.rb +1 -1
  12. data/lib/screenkit/config/project.rb +5 -1
  13. data/lib/screenkit/exporter/intro.rb +2 -3
  14. data/lib/screenkit/exporter/outro.rb +14 -4
  15. data/lib/screenkit/exporter/video.rb +6 -4
  16. data/lib/screenkit/generators/project/screenkit.yml +16 -0
  17. data/lib/screenkit/http.rb +58 -0
  18. data/lib/screenkit/resources/callout_styles/social/blog.png +0 -0
  19. data/lib/screenkit/resources/callout_styles/social/bsky.png +0 -0
  20. data/lib/screenkit/resources/callout_styles/social/discord.png +0 -0
  21. data/lib/screenkit/resources/callout_styles/social/dribbble.png +0 -0
  22. data/lib/screenkit/resources/callout_styles/social/github.png +0 -0
  23. data/lib/screenkit/resources/callout_styles/social/instagram.png +0 -0
  24. data/lib/screenkit/resources/callout_styles/social/linkedin.png +0 -0
  25. data/lib/screenkit/resources/callout_styles/social/mastodon.png +0 -0
  26. data/lib/screenkit/resources/callout_styles/social/snap.png +0 -0
  27. data/lib/screenkit/resources/callout_styles/social/soundcloud.png +0 -0
  28. data/lib/screenkit/resources/callout_styles/social/spotify.png +0 -0
  29. data/lib/screenkit/resources/callout_styles/social/tiktok.png +0 -0
  30. data/lib/screenkit/resources/callout_styles/social/twitch.png +0 -0
  31. data/lib/screenkit/resources/callout_styles/social/youtube.png +0 -0
  32. data/lib/screenkit/schemas/callout_styles/social.json +55 -0
  33. data/lib/screenkit/schemas/refs/text_style.json +4 -0
  34. data/lib/screenkit/sound.rb +1 -1
  35. data/lib/screenkit/tts/base.rb +16 -7
  36. data/lib/screenkit/tts/eleven_labs.rb +8 -23
  37. data/lib/screenkit/tts/espeak.rb +1 -1
  38. data/lib/screenkit/tts/say.rb +1 -1
  39. data/lib/screenkit/version.rb +1 -1
  40. data/lib/screenkit.rb +8 -1
  41. data/screenkit.gemspec +1 -0
  42. metadata +36 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dc4a6ebfa5fb6725aa40e4bbe699bcafca860135ef8cca01fc48ab66731392f
4
- data.tar.gz: 42df45d24c11f800e20fff4dfc030102bef5747f758ee47378d98a56dd8732f8
3
+ metadata.gz: 60085249659092fa92d7fab2c913f5e8d8fc3b8d9ce0ac467322ef590a08e5f9
4
+ data.tar.gz: 318d53f5ba88c1b8f2edd6d19ac263f2fcee0df7e7b5cb639f760b867be6e4ad
5
5
  SHA512:
6
- metadata.gz: c0eac47f990100559123217b0a54f1ae84629ea6ff7039c38d6212d72284cce07b10552f92b6504588ab74a013bb3242d8bc19fe66a7a06b0cb3a8014822248b
7
- data.tar.gz: 5c43aae473d552c8f0d233c0ab19b8ca1a1eab91b26225b7533c53eb4caeb44e5909cbc5e20c04b66bad939acee49219d0a75f61edb1617e00e209e498546567
6
+ metadata.gz: 565c78ea1e6701ee66087d22f4e62614ab765e1e625b50bdbeb9a953b3b4d7980dc6f8e7cad87250e1760f2923b10747c83c867f59347d1b4c37089d5ac87bb0
7
+ data.tar.gz: df2439bbb02d0bf47ecc80abdeafdd285f5c1f63782ce2a4411cb326a7d6b5edb6ddc79b36969555ad699439f4acea557265f52e1fc4a52be1d8e2d75e614a73
data/CHANGELOG.md CHANGED
@@ -11,6 +11,15 @@ Prefix your message with one of the following:
11
11
  - [Security] in case of vulnerabilities.
12
12
  -->
13
13
 
14
+ ## v0.0.9
15
+
16
+ - [Added] Add social callouts.
17
+ - [Added] Add align support for inline block callouts.
18
+
19
+ ## v0.0.8
20
+
21
+ - [Fixed] Further improvements to support extensions.
22
+
14
23
  ## v0.0.7
15
24
 
16
25
  - [Fixed] Improve support for extensions.
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
@@ -719,7 +822,9 @@ tts:
719
822
 
720
823
  ### ElevenLabs Engine
721
824
 
722
- Professional AI voice synthesis.
825
+ The ElevenLabs TTS engine requires an API key. Set it via the `--tts-api-key`
826
+ option when exporting an episode. The API key must be prefixed with
827
+ `eleven_labs:`, e.g. `eleven_labs:sk_c195c0131de...`.
723
828
 
724
829
  ```yaml
725
830
  tts:
@@ -764,21 +869,26 @@ module. Custom engines must implement the `generate` method:
764
869
  module ScreenKit
765
870
  module TTS
766
871
  class CustomEngine < Base
767
- extend Shell
768
-
769
872
  # Optional: Define schema path for validation
770
873
  def self.schema_path
771
- ScreenKit.root_dir.join("screenkit/schemas/tts/custom_engine.json")
874
+ File.join(__dir__, "yourschema.json")
772
875
  end
773
876
 
774
877
  # This method is required.
878
+ # The keyword arguments will be all the configuration options, plus
879
+ # api_key and segments. If you don't care about those, remember to use
880
+ # the `**` operator to ignore them.
775
881
  def self.available?(**)
776
- command_exist?("some-command")
882
+ # If you're running a local command:
883
+ # command_exist?("some-command")
884
+
885
+ # If you're using an API key:
886
+ # api_key.to_s.start_with?("#{engine_name}:")
777
887
  end
778
888
 
779
889
  # This method is required.
780
890
  def generate(text:, output_path:, log_path: nil)
781
- # Optional: validate options against JSON schema.
891
+ # Optional, but recommended: validate options against JSON schema.
782
892
  self.class.validate!(options)
783
893
 
784
894
  # If you need access to previous/next text, you can access the method
@@ -790,11 +900,19 @@ module ScreenKit
790
900
  # Write output to output_path
791
901
  # Optionally log to log_path
792
902
 
793
- # Example implementation:
903
+ # Example calling a command (provided by ScreenKit::Shell)
794
904
  # self.class.run_command "some-command",
795
905
  # "-o", output_path.sub_ext(".wav"),
796
906
  # text,
797
907
  # log_path:
908
+
909
+ # Example using an API (provided by ScreenKit::HTTP)
910
+ # response = json_post(
911
+ # url: "https://api.example.com/tts",
912
+ # headers: {authorization: "Bearer #{api_key}"},
913
+ # api_key:,
914
+ # log_path:
915
+ # )
798
916
  end
799
917
  end
800
918
  end
@@ -821,6 +939,7 @@ The engine name is camelized (e.g., `custom_engine` → `CustomEngine`,
821
939
 
822
940
  - [Search Github](https://github.com/topics/screenkit-tts)
823
941
  - [Google Text to Speech](https://github.com/fnando/screenkit-tts-google)
942
+ - [Minimax Text to Speech](https://github.com/fnando/screenkit-tts-minimax)
824
943
 
825
944
  > [!TIP]
826
945
  >
@@ -1121,26 +1240,26 @@ be processed).
1121
1240
 
1122
1241
  ### Common Issues
1123
1242
 
1124
- **"Gem not found" error:**
1243
+ #### "Gem not found" error
1125
1244
 
1126
1245
  ```bash
1127
1246
  bundle install
1128
1247
  bundle exec screenkit ...
1129
1248
  ```
1130
1249
 
1131
- **"Schema validation failed":**
1250
+ #### Schema validation failed
1132
1251
 
1133
1252
  - Check YAML syntax
1134
1253
  - Verify required fields are present
1135
1254
  - Use schema hints with `yaml-language-server`
1136
1255
 
1137
- **Missing resources:**
1256
+ #### Missing resources
1138
1257
 
1139
1258
  - Check `resources_dir` configuration
1140
1259
  - Verify file paths are relative to resource directories
1141
1260
  - Use absolute paths for system resources
1142
1261
 
1143
- **TTS not working:**
1262
+ #### TTS not working
1144
1263
 
1145
1264
  - For ElevenLabs: Set `--tts-api-key`
1146
1265
  - For macOS `say`: Verify voice name with `say -v ?`
@@ -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(
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScreenKit
4
+ module HTTP
5
+ # Sends a POST request.
6
+ # @param url [String] The request URL.
7
+ # @param params [Hash] The request parameters.
8
+ # @param headers [Hash] The request headers.
9
+ # @param api_key [String] The API key to redact from logs.
10
+ # @param log_path [String, nil] The path to log the request details.
11
+ # @return [Aitch::Response] The response.
12
+ def post(url:, params:, headers:, api_key:, log_path: nil)
13
+ if log_path
14
+ File.open(log_path, "w") do |f|
15
+ f << JSON.pretty_generate(url:, params:, headers:)
16
+ end
17
+ end
18
+
19
+ client = Aitch::Namespace.new
20
+ client.configure do |config|
21
+ config.logger = Logger.new(log_path) if log_path
22
+ end
23
+
24
+ client.post(
25
+ url:,
26
+ params:,
27
+ options: {expect: 200},
28
+ headers: headers.merge(user_agent: "ScreenKit/#{ScreenKit::VERSION}")
29
+ )
30
+ ensure
31
+ redact_file(log_path, api_key)
32
+ end
33
+
34
+ # Sends a JSON POST request.
35
+ # @param url [String] The request URL.
36
+ # @param params [Hash] The request parameters.
37
+ # @param headers [Hash] The request headers.
38
+ # @param api_key [String] The API key to redact from logs.
39
+ # @param log_path [String, nil] The path to log the request details.
40
+ # @return [Aitch::Response] The response.
41
+ def json_post(headers:, **)
42
+ headers = headers.merge(content_type: "application/json")
43
+ post(headers:, **)
44
+ end
45
+
46
+ # Redacts sensitive text from a file.
47
+ # @param path [String] The file path.
48
+ # @param text [String] The text to redact.
49
+ # @return [void]
50
+ def redact_file(path, text)
51
+ return unless path
52
+ return unless File.file?(path)
53
+
54
+ content = File.read(path).gsub(text, "[REDACTED]")
55
+ File.write(path, content)
56
+ end
57
+ end
58
+ 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)
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json-schema"
4
+ require "aitch"
5
+ require "logger"
6
+
3
7
  module ScreenKit
4
8
  module TTS
5
9
  class Base
6
- require_relative "../core_ext"
7
10
  require_relative "../schema_validator"
11
+ require_relative "../shell"
12
+ require_relative "../http"
13
+ require_relative "../core_ext"
14
+ require_relative "../version"
8
15
 
9
16
  extend SchemaValidator
17
+ extend Shell
18
+ include HTTP
10
19
 
11
20
  using CoreExt
12
21
 
@@ -33,18 +42,18 @@ module ScreenKit
33
42
  name.split("::").last.underscore
34
43
  end
35
44
 
45
+ def self.api_key_prefix
46
+ "#{engine_name}:"
47
+ end
48
+
36
49
  def initialize(id: nil, segments: nil, api_key: nil, **options)
37
50
  @segments = Array(segments)
38
51
  @options = options
39
52
  @id = id
40
- @api_key = api_key
41
- end
42
53
 
43
- def redact_file(path, text)
44
- return unless File.file?(path)
54
+ return unless api_key
45
55
 
46
- content = File.read(path).gsub(text, "[REDACTED]")
47
- File.write(path, content)
56
+ @api_key = api_key.delete_prefix("#{self.class.engine_name}:")
48
57
  end
49
58
  end
50
59
  end
@@ -3,13 +3,15 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class ElevenLabs < Base
6
+ include HTTP
7
+
6
8
  def self.schema_path
7
9
  ScreenKit.root_dir
8
10
  .join("screenkit/schemas/tts/elevenlabs.json")
9
11
  end
10
12
 
11
13
  def self.available?(api_key: nil, **)
12
- api_key.to_s.empty?
14
+ api_key.to_s.start_with?(api_key_prefix)
13
15
  end
14
16
 
15
17
  def all_texts
@@ -19,18 +21,6 @@ module ScreenKit
19
21
  def generate(output_path:, text:, log_path: nil)
20
22
  voice_id = options[:voice_id]
21
23
 
22
- if log_path
23
- File.open(log_path, "w") do |f|
24
- f << JSON.pretty_generate(options.merge(text:))
25
- end
26
- end
27
-
28
- require "aitch"
29
-
30
- Aitch.configure do |config|
31
- config.logger = Logger.new(log_path) if log_path
32
- end
33
-
34
24
  current_index = all_texts.index { it == text }
35
25
 
36
26
  if current_index
@@ -41,20 +31,15 @@ module ScreenKit
41
31
  params = options.merge(text:, previous_text:, next_text:)
42
32
  .except(:voice_id)
43
33
 
44
- response = Aitch.post(
34
+ response = json_post(
45
35
  url: "https://api.elevenlabs.io/v1/text-to-speech/#{voice_id}",
46
- body: JSON.dump(params),
47
- options: {expect: 200},
48
- headers: {
49
- "content-type": "application/json",
50
- "user-agent": "ScreenKit/#{ScreenKit::VERSION}",
51
- "xi-api-key": api_key
52
- }
36
+ params:,
37
+ headers: {"xi-api-key": api_key},
38
+ api_key:,
39
+ log_path:
53
40
  )
54
41
 
55
42
  File.binwrite(output_path, response.body)
56
- ensure
57
- redact_file(log_path, api_key)
58
43
  end
59
44
  end
60
45
  end
@@ -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.7"
4
+ VERSION = "0.0.9"
5
5
  end
data/lib/screenkit.rb CHANGED
@@ -10,6 +10,8 @@ require "tty-spinner"
10
10
  require "etc"
11
11
  require "securerandom"
12
12
  require "demo_tape/duration"
13
+ require "aitch"
14
+ require "erb"
13
15
 
14
16
  module ScreenKit
15
17
  require_relative "screenkit/version"
@@ -23,6 +25,7 @@ module ScreenKit
23
25
  require_relative "screenkit/watermark"
24
26
  require_relative "screenkit/spinner"
25
27
  require_relative "screenkit/shell"
28
+ require_relative "screenkit/http"
26
29
  require_relative "screenkit/schema_validator"
27
30
  require_relative "screenkit/generators/project"
28
31
  require_relative "screenkit/generators/episode"
@@ -65,7 +68,11 @@ module ScreenKit
65
68
  require_files.call("screenkit/tts/*.rb")
66
69
 
67
70
  def self.root_dir
68
- @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")
69
76
  end
70
77
 
71
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.7
4
+ version: 0.0.9
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
@@ -328,15 +343,31 @@ files:
328
343
  - lib/screenkit/generators/project/resources/sounds/pop.mp3
329
344
  - lib/screenkit/generators/project/resources/sounds/whoosh.mp3
330
345
  - lib/screenkit/generators/project/screenkit.yml
346
+ - lib/screenkit/http.rb
331
347
  - lib/screenkit/logfile.rb
332
348
  - lib/screenkit/parallel_processor.rb
333
349
  - lib/screenkit/path_lookup.rb
350
+ - lib/screenkit/resources/callout_styles/social/blog.png
351
+ - lib/screenkit/resources/callout_styles/social/bsky.png
352
+ - lib/screenkit/resources/callout_styles/social/discord.png
353
+ - lib/screenkit/resources/callout_styles/social/dribbble.png
354
+ - lib/screenkit/resources/callout_styles/social/github.png
355
+ - lib/screenkit/resources/callout_styles/social/instagram.png
356
+ - lib/screenkit/resources/callout_styles/social/linkedin.png
357
+ - lib/screenkit/resources/callout_styles/social/mastodon.png
358
+ - lib/screenkit/resources/callout_styles/social/snap.png
359
+ - lib/screenkit/resources/callout_styles/social/soundcloud.png
360
+ - lib/screenkit/resources/callout_styles/social/spotify.png
361
+ - lib/screenkit/resources/callout_styles/social/tiktok.png
362
+ - lib/screenkit/resources/callout_styles/social/twitch.png
363
+ - lib/screenkit/resources/callout_styles/social/youtube.png
334
364
  - lib/screenkit/resources/mute.mp3
335
365
  - lib/screenkit/resources/transparent.png
336
366
  - lib/screenkit/schema_validator.rb
337
367
  - lib/screenkit/schemas/callout_styles/file_copy.json
338
368
  - lib/screenkit/schemas/callout_styles/inline_block.json
339
369
  - lib/screenkit/schemas/callout_styles/shadow_block.json
370
+ - lib/screenkit/schemas/callout_styles/social.json
340
371
  - lib/screenkit/schemas/callouts/inline_block.json
341
372
  - lib/screenkit/schemas/callouts/shadow_block.json
342
373
  - lib/screenkit/schemas/episode.json
@@ -387,10 +418,10 @@ metadata:
387
418
  rubygems_mfa_required: 'true'
388
419
  homepage_uri: https://github.com/fnando/screenkit
389
420
  bug_tracker_uri: https://github.com/fnando/screenkit/issues
390
- source_code_uri: https://github.com/fnando/screenkit/tree/v0.0.7
391
- changelog_uri: https://github.com/fnando/screenkit/tree/v0.0.7/CHANGELOG.md
392
- documentation_uri: https://github.com/fnando/screenkit/tree/v0.0.7/README.md
393
- license_uri: https://github.com/fnando/screenkit/tree/v0.0.7/LICENSE.md
421
+ source_code_uri: https://github.com/fnando/screenkit/tree/v0.0.9
422
+ changelog_uri: https://github.com/fnando/screenkit/tree/v0.0.9/CHANGELOG.md
423
+ documentation_uri: https://github.com/fnando/screenkit/tree/v0.0.9/README.md
424
+ license_uri: https://github.com/fnando/screenkit/tree/v0.0.9/LICENSE.md
394
425
  rdoc_options: []
395
426
  require_paths:
396
427
  - lib