screenkit 0.0.0
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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +4 -0
- data/.github/FUNDING.yml +4 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +38 -0
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/ruby-tests.yml +51 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +16 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +80 -0
- data/DOCUMENTATION.md +972 -0
- data/Gemfile +5 -0
- data/LICENSE.md +20 -0
- data/README.md +49 -0
- data/Rakefile +15 -0
- data/bin/console +16 -0
- data/bin/setup +10 -0
- data/exe/screenkit +5 -0
- data/lib/screen_kit.rb +79 -0
- data/lib/screenkit/anchor.rb +19 -0
- data/lib/screenkit/animation_filters.rb +114 -0
- data/lib/screenkit/banner.rb +46 -0
- data/lib/screenkit/callout/styles/base.rb +101 -0
- data/lib/screenkit/callout/styles/default.rb +144 -0
- data/lib/screenkit/callout/styles/inline_block.rb +123 -0
- data/lib/screenkit/callout/text_style.rb +44 -0
- data/lib/screenkit/callout.rb +98 -0
- data/lib/screenkit/cli/base.rb +24 -0
- data/lib/screenkit/cli/episode.rb +78 -0
- data/lib/screenkit/cli/root.rb +73 -0
- data/lib/screenkit/cli.rb +9 -0
- data/lib/screenkit/config/base.rb +51 -0
- data/lib/screenkit/config/episode.rb +42 -0
- data/lib/screenkit/config/project.rb +47 -0
- data/lib/screenkit/content_type.rb +12 -0
- data/lib/screenkit/core_ext/json.rb +47 -0
- data/lib/screenkit/core_ext/string.rb +28 -0
- data/lib/screenkit/exporter/demotape.rb +26 -0
- data/lib/screenkit/exporter/episode.rb +565 -0
- data/lib/screenkit/exporter/image.rb +31 -0
- data/lib/screenkit/exporter/intro.rb +261 -0
- data/lib/screenkit/exporter/outro.rb +183 -0
- data/lib/screenkit/exporter/segment.rb +258 -0
- data/lib/screenkit/exporter/video.rb +33 -0
- data/lib/screenkit/generators/episode/config.yml.erb +106 -0
- data/lib/screenkit/generators/episode/content/001.tape +4 -0
- data/lib/screenkit/generators/episode/scripts/001.txt +1 -0
- data/lib/screenkit/generators/episode.rb +31 -0
- data/lib/screenkit/generators/project/Gemfile.erb +7 -0
- data/lib/screenkit/generators/project/resources/backtracks/default.aac +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OFL.txt +93 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Bold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-BoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-ExtraBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-ExtraBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Italic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Light.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-LightItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Medium.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-MediumItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Regular.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-SemiBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-SemiBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Bold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-BoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Italic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Light.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-LightItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Medium.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-MediumItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Regular.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Bold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-BoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Italic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Light.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-LightItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Medium.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-MediumItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Regular.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/README.txt +100 -0
- data/lib/screenkit/generators/project/resources/images/logo.png +0 -0
- data/lib/screenkit/generators/project/resources/images/watermark.png +0 -0
- data/lib/screenkit/generators/project/resources/sounds/chime.mp3 +0 -0
- data/lib/screenkit/generators/project/resources/sounds/pop.mp3 +0 -0
- data/lib/screenkit/generators/project/resources/sounds/whoosh.mp3 +0 -0
- data/lib/screenkit/generators/project/screenkit.yml +189 -0
- data/lib/screenkit/generators/project.rb +34 -0
- data/lib/screenkit/logfile.rb +33 -0
- data/lib/screenkit/parallel_processor.rb +50 -0
- data/lib/screenkit/path_lookup.rb +27 -0
- data/lib/screenkit/resources/mute.mp3 +0 -0
- data/lib/screenkit/resources/transparent.png +0 -0
- data/lib/screenkit/schema_validator.rb +14 -0
- data/lib/screenkit/schemas/callouts/default.json +44 -0
- data/lib/screenkit/schemas/callouts/inline_block.json +20 -0
- data/lib/screenkit/schemas/episode.json +74 -0
- data/lib/screenkit/schemas/project.json +37 -0
- data/lib/screenkit/schemas/refs/anchor.json +17 -0
- data/lib/screenkit/schemas/refs/animation.json +7 -0
- data/lib/screenkit/schemas/refs/background.json +14 -0
- data/lib/screenkit/schemas/refs/callout.json +27 -0
- data/lib/screenkit/schemas/refs/color.json +7 -0
- data/lib/screenkit/schemas/refs/directory.json +20 -0
- data/lib/screenkit/schemas/refs/intro.json +30 -0
- data/lib/screenkit/schemas/refs/logo.json +26 -0
- data/lib/screenkit/schemas/refs/outro.json +26 -0
- data/lib/screenkit/schemas/refs/position.json +38 -0
- data/lib/screenkit/schemas/refs/scenes.json +27 -0
- data/lib/screenkit/schemas/refs/size.json +19 -0
- data/lib/screenkit/schemas/refs/sound.json +36 -0
- data/lib/screenkit/schemas/refs/spacing.json +22 -0
- data/lib/screenkit/schemas/refs/text_style.json +18 -0
- data/lib/screenkit/schemas/refs/transition.json +15 -0
- data/lib/screenkit/schemas/refs/tts.json +47 -0
- data/lib/screenkit/schemas/refs/watermark.json +18 -0
- data/lib/screenkit/schemas/tts/elevenlabs.json +67 -0
- data/lib/screenkit/schemas/tts/say.json +16 -0
- data/lib/screenkit/shell.rb +58 -0
- data/lib/screenkit/sound.rb +44 -0
- data/lib/screenkit/spacing.rb +23 -0
- data/lib/screenkit/spinner.rb +39 -0
- data/lib/screenkit/transition.rb +16 -0
- data/lib/screenkit/tts/eleven_labs.rb +51 -0
- data/lib/screenkit/tts/say.rb +31 -0
- data/lib/screenkit/utils.rb +87 -0
- data/lib/screenkit/version.rb +5 -0
- data/lib/screenkit/watermark.rb +34 -0
- data/lib/screenkit.rb +3 -0
- data/screenkit.gemspec +56 -0
- metadata +426 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mini_magick"
|
|
4
|
+
|
|
5
|
+
module ScreenKit
|
|
6
|
+
class Callout
|
|
7
|
+
module Styles
|
|
8
|
+
class InlineBlock < Base
|
|
9
|
+
extend SchemaValidator
|
|
10
|
+
|
|
11
|
+
attr_reader :background_color, :text_style, :body,
|
|
12
|
+
:output_path, :padding, :text, :width, :source
|
|
13
|
+
|
|
14
|
+
def self.schema_path
|
|
15
|
+
ScreenKit.root_dir
|
|
16
|
+
.join("screenkit/schemas/callouts/inline_block.json")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(source:, **kwargs) # rubocop:disable Lint/MissingSuper
|
|
20
|
+
self.class.validate!(kwargs)
|
|
21
|
+
|
|
22
|
+
@source = source
|
|
23
|
+
|
|
24
|
+
# Set default values
|
|
25
|
+
kwargs = hi_res({
|
|
26
|
+
text_style: {size: 50, color: "#ffffff"}.merge(text_style || {}),
|
|
27
|
+
width: 600,
|
|
28
|
+
padding: [10, 10, 10, 10],
|
|
29
|
+
background_color: "#000000"
|
|
30
|
+
}.merge(kwargs))
|
|
31
|
+
|
|
32
|
+
kwargs.each do |key, value|
|
|
33
|
+
value = case key
|
|
34
|
+
when :padding
|
|
35
|
+
Spacing.new(value)
|
|
36
|
+
when :text_style
|
|
37
|
+
TextStyle.new(source:, **value)
|
|
38
|
+
else
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
instance_variable_set(:"@#{key}", value)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def as_json(*)
|
|
47
|
+
{
|
|
48
|
+
background_color:,
|
|
49
|
+
text_style: text_style.as_json,
|
|
50
|
+
output_path:,
|
|
51
|
+
padding: padding.as_json,
|
|
52
|
+
text:,
|
|
53
|
+
width:
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render
|
|
58
|
+
padding_x = padding.horizontal
|
|
59
|
+
padding_y = padding.vertical
|
|
60
|
+
content_width = width - padding_x
|
|
61
|
+
lines = if text.include?("\n")
|
|
62
|
+
text.lines.map(&:strip)
|
|
63
|
+
else
|
|
64
|
+
text_wrap(
|
|
65
|
+
text,
|
|
66
|
+
max_width: content_width,
|
|
67
|
+
font_size: text_style.size
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
line_images = lines.map do |line|
|
|
72
|
+
render_text_image(
|
|
73
|
+
type: "label",
|
|
74
|
+
text: line,
|
|
75
|
+
style: text_style,
|
|
76
|
+
width: content_width
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
max_line_width = line_images.map {|_, w, _| w }.max || 0
|
|
81
|
+
height = line_images.sum {|_, _, h| h }
|
|
82
|
+
|
|
83
|
+
offset_y = 0
|
|
84
|
+
image_width = max_line_width + padding_x
|
|
85
|
+
image_height = (padding_y * lines.size) + height
|
|
86
|
+
|
|
87
|
+
MiniMagick.convert do |image|
|
|
88
|
+
# Create transparent canvas
|
|
89
|
+
image << "-size"
|
|
90
|
+
image << "#{image_width}x#{image_height}"
|
|
91
|
+
image << "xc:none"
|
|
92
|
+
|
|
93
|
+
line_images.each do |path, width, height|
|
|
94
|
+
# Draw rectangle background
|
|
95
|
+
image << "-fill"
|
|
96
|
+
image << background_color
|
|
97
|
+
image << "-draw"
|
|
98
|
+
image << "rectangle 0,#{offset_y}," \
|
|
99
|
+
"#{width + padding_x}," \
|
|
100
|
+
"#{offset_y + height + padding_y}"
|
|
101
|
+
|
|
102
|
+
# Composite line text
|
|
103
|
+
image << path
|
|
104
|
+
image << "-geometry"
|
|
105
|
+
image << "+#{padding.top}+#{offset_y + padding.left}"
|
|
106
|
+
image << "-composite"
|
|
107
|
+
offset_y += padding_y + height
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
image << "PNG:#{output_path}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
output_path
|
|
114
|
+
rescue MiniMagick::Error => error
|
|
115
|
+
retry if error.message.include?("No such file or directory")
|
|
116
|
+
raise
|
|
117
|
+
ensure
|
|
118
|
+
line_images&.each {|(path)| remove_file(path) }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
class Callout
|
|
5
|
+
class TextStyle
|
|
6
|
+
attr_reader :color, :size, :font_path
|
|
7
|
+
|
|
8
|
+
def initialize(source:, **kwargs)
|
|
9
|
+
@source = source
|
|
10
|
+
|
|
11
|
+
kwargs.each do |key, value|
|
|
12
|
+
value = case key.to_sym
|
|
13
|
+
when :font_path
|
|
14
|
+
@source.search(value)
|
|
15
|
+
else
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
instance_variable_set(:"@#{key}", value)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Convert hex color (with optional alpha) to RGB + opacity
|
|
24
|
+
# #RRGGBB or #RRGGBBAA
|
|
25
|
+
def rgb_color
|
|
26
|
+
color.match(/#([0-9a-fA-F]{6})/) {|m| m[1] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def opacity
|
|
30
|
+
if color.length == 9
|
|
31
|
+
color.match(/#[0-9a-fA-F]{6}([0-9a-fA-F]{2})/) do |m|
|
|
32
|
+
m[1].to_i(16) / 255.0
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
1.0
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def as_json(*)
|
|
40
|
+
{color:, size:, font_path:, rgb_color:, opacity:}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
class Callout
|
|
5
|
+
using CoreExt
|
|
6
|
+
|
|
7
|
+
# Raised when a style class cannot be found
|
|
8
|
+
UndefinedStyleError = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
extend SchemaValidator
|
|
11
|
+
|
|
12
|
+
# The callout anchor position. Should be an array with two elements:
|
|
13
|
+
#
|
|
14
|
+
# - Horizontal position: "left", "center", or "right"
|
|
15
|
+
# - Vertical position: "top", "center", or "bottom"
|
|
16
|
+
#
|
|
17
|
+
# @return [Array<String>] e.g., `["left", "bottom"]`
|
|
18
|
+
attr_reader :anchor
|
|
19
|
+
|
|
20
|
+
# The callout margin around the edges. Can be either a number, or an array
|
|
21
|
+
# with 1-4 elements, representing CSS-style margin values.
|
|
22
|
+
#
|
|
23
|
+
# - 1 value: all sides
|
|
24
|
+
# - 2 values: vertical | horizontal
|
|
25
|
+
# - 3 values: top | horizontal | bottom
|
|
26
|
+
# - 4 values: top | right | bottom | left
|
|
27
|
+
#
|
|
28
|
+
# @return [Array<Integer>, Integer] e.g., `20`, `[10, 20, 10, 20]`
|
|
29
|
+
attr_reader :margin
|
|
30
|
+
|
|
31
|
+
attr_accessor :in_transition, :out_transition, :style,
|
|
32
|
+
:style_props, :style_class, :animation, :source, :log_path
|
|
33
|
+
|
|
34
|
+
def self.schema_path
|
|
35
|
+
ScreenKit.root_dir.join("screenkit/schemas/refs/callout.json")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(
|
|
39
|
+
source:,
|
|
40
|
+
animation:,
|
|
41
|
+
anchor:,
|
|
42
|
+
in_transition:,
|
|
43
|
+
out_transition:,
|
|
44
|
+
margin:,
|
|
45
|
+
style: "default",
|
|
46
|
+
log_path: nil,
|
|
47
|
+
**style_props
|
|
48
|
+
)
|
|
49
|
+
style_name = style || "default"
|
|
50
|
+
|
|
51
|
+
self.class.validate!(
|
|
52
|
+
animation:,
|
|
53
|
+
style: style_name,
|
|
54
|
+
anchor:,
|
|
55
|
+
in_transition:,
|
|
56
|
+
out_transition:,
|
|
57
|
+
margin:
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@log_path = log_path
|
|
61
|
+
@source = source
|
|
62
|
+
@animation = animation
|
|
63
|
+
@style_class = resolve_style_class(style_name)
|
|
64
|
+
@anchor = Anchor.new(anchor)
|
|
65
|
+
@margin = Spacing.new(margin)
|
|
66
|
+
@in_transition = Transition.new(**in_transition)
|
|
67
|
+
@out_transition = Transition.new(**out_transition)
|
|
68
|
+
@style = style_class.new(source:, **style_props)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render
|
|
72
|
+
if log_path
|
|
73
|
+
File.open(log_path, "w") do |f|
|
|
74
|
+
f << JSON.pretty_generate(
|
|
75
|
+
animation:,
|
|
76
|
+
anchor:,
|
|
77
|
+
margin:,
|
|
78
|
+
in_transition:,
|
|
79
|
+
out_transition:,
|
|
80
|
+
style:
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
style.render
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_style_class(style)
|
|
89
|
+
error_message = "Style #{style.inspect} is not defined"
|
|
90
|
+
|
|
91
|
+
raise UndefinedStyleError, error_message unless style
|
|
92
|
+
|
|
93
|
+
Styles.const_get(style.split("_").map(&:capitalize).join)
|
|
94
|
+
rescue NameError
|
|
95
|
+
raise UndefinedStyleError, error_message
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module CLI
|
|
5
|
+
class Base < Thor
|
|
6
|
+
check_unknown_options!
|
|
7
|
+
|
|
8
|
+
class_option :config,
|
|
9
|
+
type: :string,
|
|
10
|
+
default: "screenkit.yml",
|
|
11
|
+
desc: "Path to config file"
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
no_commands do
|
|
18
|
+
def config
|
|
19
|
+
@config ||= Config::Project.load_file(options.config)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module CLI
|
|
5
|
+
class Episode < Base
|
|
6
|
+
namespace :episode
|
|
7
|
+
using CoreExt
|
|
8
|
+
|
|
9
|
+
desc "new", "Create a new episode"
|
|
10
|
+
option :title,
|
|
11
|
+
type: :string,
|
|
12
|
+
required: true,
|
|
13
|
+
desc: "Title of the episode"
|
|
14
|
+
def new
|
|
15
|
+
options = self.options.dup
|
|
16
|
+
episode_number = config.episode_dir.parent.glob("*").count(&:directory?)
|
|
17
|
+
|
|
18
|
+
dir = format(
|
|
19
|
+
config.episode_dir.basename.to_s,
|
|
20
|
+
episode_number: episode_number + 1,
|
|
21
|
+
episode_slug: options.title.dasherize,
|
|
22
|
+
date: Time.now.strftime("%Y-%m-%d")
|
|
23
|
+
)
|
|
24
|
+
options[:episode_dir] = config.episode_dir.parent.join(dir)
|
|
25
|
+
|
|
26
|
+
generator = Generators::Episode.new
|
|
27
|
+
generator.destination_root =
|
|
28
|
+
File.expand_path(File.dirname(options.config))
|
|
29
|
+
generator.options = options
|
|
30
|
+
generator.invoke_all
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "export", "Export episode into a final video file"
|
|
34
|
+
option :dir,
|
|
35
|
+
type: :string,
|
|
36
|
+
required: true,
|
|
37
|
+
desc: "Directory of the episode to export"
|
|
38
|
+
option :voice_api_key,
|
|
39
|
+
type: :string,
|
|
40
|
+
desc: "API key for the voice synthesis service"
|
|
41
|
+
option :overwrite,
|
|
42
|
+
type: :boolean,
|
|
43
|
+
default: false,
|
|
44
|
+
desc: "Overwrite existing exported file"
|
|
45
|
+
option :match_segment,
|
|
46
|
+
type: :string,
|
|
47
|
+
desc: "Only export segments matching this string"
|
|
48
|
+
option :output_dir,
|
|
49
|
+
type: :string,
|
|
50
|
+
desc: "Path to save the exported video files"
|
|
51
|
+
option :banner,
|
|
52
|
+
type: :boolean,
|
|
53
|
+
default: true,
|
|
54
|
+
desc: "Display the ScreenKit banner"
|
|
55
|
+
option :require,
|
|
56
|
+
type: :array,
|
|
57
|
+
default: [],
|
|
58
|
+
desc: "Additional Ruby files to require"
|
|
59
|
+
def export
|
|
60
|
+
puts Banner.banner if options.banner
|
|
61
|
+
|
|
62
|
+
episode_config = Config::Episode.load_file(
|
|
63
|
+
File.join(options.dir, "config.yml")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
options.require.each { require(it) }
|
|
67
|
+
|
|
68
|
+
exporter = ScreenKit::Exporter::Episode.new(
|
|
69
|
+
project_config: config,
|
|
70
|
+
config: episode_config,
|
|
71
|
+
options:
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
exporter.export
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module CLI
|
|
5
|
+
class Root < Base
|
|
6
|
+
desc "episode SUBCOMMAND", "Episode commands"
|
|
7
|
+
subcommand "episode", CLI::Episode
|
|
8
|
+
|
|
9
|
+
desc "new PATH", "Create a new project"
|
|
10
|
+
def new(path)
|
|
11
|
+
generator = Generators::Project.new
|
|
12
|
+
generator.destination_root = File.expand_path(path)
|
|
13
|
+
generator.options = options
|
|
14
|
+
generator.invoke_all
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "callout", "Generate a callout PNG"
|
|
18
|
+
option :type,
|
|
19
|
+
type: :string,
|
|
20
|
+
required: true,
|
|
21
|
+
desc: "Callout type (e.g., info, warning)"
|
|
22
|
+
option :title,
|
|
23
|
+
type: :string,
|
|
24
|
+
required: true,
|
|
25
|
+
desc: "Callout title text"
|
|
26
|
+
option :body,
|
|
27
|
+
type: :string,
|
|
28
|
+
required: true,
|
|
29
|
+
desc: "Callout body text"
|
|
30
|
+
option :output,
|
|
31
|
+
type: :string,
|
|
32
|
+
desc: "Output path for PNG"
|
|
33
|
+
def callout
|
|
34
|
+
callout = config.callouts[options.type.to_sym]
|
|
35
|
+
|
|
36
|
+
unless callout
|
|
37
|
+
say "Callout type '#{options[:type]}' not found in config", :red
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
output_path = options[:output]
|
|
42
|
+
output_path ||= Pathname(Tempfile.create(["callout-", ".png"]).path)
|
|
43
|
+
|
|
44
|
+
callout.render(
|
|
45
|
+
output_path:,
|
|
46
|
+
title: options.title,
|
|
47
|
+
body: options.body
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
puts output_path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "completion", "Generate shell completion script"
|
|
54
|
+
option :shell,
|
|
55
|
+
type: :string,
|
|
56
|
+
required: true,
|
|
57
|
+
enum: %w[bash zsh powershell fish]
|
|
58
|
+
def completion
|
|
59
|
+
puts Thor::Completion.generate(
|
|
60
|
+
name: "screenkit",
|
|
61
|
+
description: "Terminal to screencast, simplified",
|
|
62
|
+
version: VERSION,
|
|
63
|
+
cli: self.class,
|
|
64
|
+
shell: options.shell
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
no_commands do
|
|
69
|
+
# Add helper methods here
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module Config
|
|
5
|
+
class Base
|
|
6
|
+
extend SchemaValidator
|
|
7
|
+
|
|
8
|
+
def self.load_file(path)
|
|
9
|
+
unless File.file?(path)
|
|
10
|
+
raise FileNotFoundError, "Config file not found: #{path}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
config = YAML.load_file(path, symbolize_names: true)
|
|
14
|
+
load(config)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.load(config)
|
|
18
|
+
validate!(config)
|
|
19
|
+
|
|
20
|
+
new(**config)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(**kwargs)
|
|
24
|
+
kwargs.each do |key, value|
|
|
25
|
+
value = process(key, value)
|
|
26
|
+
instance_variable_set(:"@#{key}", value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def process(_key, value)
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
36
|
+
key = var.to_s.delete_prefix("@").to_sym
|
|
37
|
+
value = instance_variable_get(var)
|
|
38
|
+
|
|
39
|
+
hash[key] =
|
|
40
|
+
if value.respond_to?(:as_json)
|
|
41
|
+
value.as_json
|
|
42
|
+
elsif value.is_a?(Array)
|
|
43
|
+
value.map {|v| v.respond_to?(:as_json) ? v.as_json : v }
|
|
44
|
+
else
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module Config
|
|
5
|
+
class Episode < Base
|
|
6
|
+
# The scenes configuration for the episode.
|
|
7
|
+
attr_reader :scenes
|
|
8
|
+
|
|
9
|
+
# The title of the episode.
|
|
10
|
+
attr_reader :title
|
|
11
|
+
|
|
12
|
+
# The episode's TTS engine configuration.
|
|
13
|
+
attr_reader :tts
|
|
14
|
+
|
|
15
|
+
# The episode's backtrack music configuration.
|
|
16
|
+
attr_reader :backtrack
|
|
17
|
+
|
|
18
|
+
# The watermark configuration.
|
|
19
|
+
attr_reader :watermark
|
|
20
|
+
|
|
21
|
+
def self.schema_path
|
|
22
|
+
@schema_path ||=
|
|
23
|
+
ScreenKit.root_dir.join("screenkit/schemas/episode.json")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(**)
|
|
27
|
+
@scenes = {}
|
|
28
|
+
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process(key, value)
|
|
33
|
+
case key.to_sym
|
|
34
|
+
when /_(dir|path)$/
|
|
35
|
+
Pathname(value)
|
|
36
|
+
else
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module Config
|
|
5
|
+
class Project < Base
|
|
6
|
+
# The directory where episode source files are stored.
|
|
7
|
+
attr_reader :episode_dir
|
|
8
|
+
|
|
9
|
+
# The directory where resources files are stored.
|
|
10
|
+
attr_reader :resources_dir
|
|
11
|
+
|
|
12
|
+
# The output directory for exported files.
|
|
13
|
+
attr_reader :output_dir
|
|
14
|
+
|
|
15
|
+
# Callout configurations
|
|
16
|
+
attr_reader :callouts
|
|
17
|
+
|
|
18
|
+
# Scene configurations
|
|
19
|
+
attr_reader :scenes
|
|
20
|
+
|
|
21
|
+
# TTS configuration
|
|
22
|
+
attr_reader :tts
|
|
23
|
+
|
|
24
|
+
# The backtrack music configuration.
|
|
25
|
+
attr_reader :backtrack
|
|
26
|
+
|
|
27
|
+
# The watermark configuration.
|
|
28
|
+
attr_reader :watermark
|
|
29
|
+
|
|
30
|
+
def self.schema_path
|
|
31
|
+
@schema_path ||=
|
|
32
|
+
ScreenKit.root_dir.join("screenkit/schemas/project.json")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private def process(key, value)
|
|
36
|
+
case key.to_sym
|
|
37
|
+
when :resources_dir
|
|
38
|
+
Array(value)
|
|
39
|
+
when /_(dir|path)$/
|
|
40
|
+
Pathname(value)
|
|
41
|
+
else
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module ContentType
|
|
5
|
+
def self.video = %w[mp4 webm mov]
|
|
6
|
+
def self.audio = %w[mp3 wav m4a aac aiff]
|
|
7
|
+
def self.image = %w[gif jpg jpeg png]
|
|
8
|
+
def self.demotape = %w[tape]
|
|
9
|
+
|
|
10
|
+
def self.all = video + image + demotape
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module CoreExt
|
|
5
|
+
refine JSON.singleton_class do
|
|
6
|
+
def pretty_generate(target, *)
|
|
7
|
+
super(target.as_json, *)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def dump(target, *, **)
|
|
11
|
+
super(target.as_json, *, **)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
refine Object do
|
|
16
|
+
def to_json(*)
|
|
17
|
+
if respond_to?(:as_json)
|
|
18
|
+
as_json(*).to_json
|
|
19
|
+
else
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def as_json(*)
|
|
25
|
+
if respond_to?(:to_h)
|
|
26
|
+
to_h.transform_values { it.as_json(*) }
|
|
27
|
+
elsif respond_to?(:to_a)
|
|
28
|
+
to_a.map { it.as_json(*) }
|
|
29
|
+
else
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
refine Hash do
|
|
36
|
+
def as_json(*)
|
|
37
|
+
transform_values { it.as_json(*) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
refine Array do
|
|
42
|
+
def as_json(*)
|
|
43
|
+
map { it.as_json(*) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenKit
|
|
4
|
+
module CoreExt
|
|
5
|
+
refine String do
|
|
6
|
+
def dasherize
|
|
7
|
+
unicode_normalize(:nfkd)
|
|
8
|
+
.delete("'")
|
|
9
|
+
.gsub(/[^\x00-\x7F]/, "")
|
|
10
|
+
.gsub(/[^-\w]+/xim, "-")
|
|
11
|
+
.tr("_", "-")
|
|
12
|
+
.gsub(/-+/xm, "-")
|
|
13
|
+
.gsub(/^-?(.*?)-?$/, '\1')
|
|
14
|
+
.downcase
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def camelize(first_letter = :upper)
|
|
18
|
+
split(/_|-/).map.with_index do |part, index|
|
|
19
|
+
if index.zero? && first_letter == :lower
|
|
20
|
+
part.downcase
|
|
21
|
+
else
|
|
22
|
+
part.capitalize
|
|
23
|
+
end
|
|
24
|
+
end.join
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|