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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +4 -0
  3. data/.github/FUNDING.yml +4 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  5. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +38 -0
  8. data/.github/dependabot.yml +15 -0
  9. data/.github/workflows/ruby-tests.yml +51 -0
  10. data/.gitignore +12 -0
  11. data/.rubocop.yml +14 -0
  12. data/CHANGELOG.md +16 -0
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/CONTRIBUTING.md +80 -0
  15. data/DOCUMENTATION.md +972 -0
  16. data/Gemfile +5 -0
  17. data/LICENSE.md +20 -0
  18. data/README.md +49 -0
  19. data/Rakefile +15 -0
  20. data/bin/console +16 -0
  21. data/bin/setup +10 -0
  22. data/exe/screenkit +5 -0
  23. data/lib/screen_kit.rb +79 -0
  24. data/lib/screenkit/anchor.rb +19 -0
  25. data/lib/screenkit/animation_filters.rb +114 -0
  26. data/lib/screenkit/banner.rb +46 -0
  27. data/lib/screenkit/callout/styles/base.rb +101 -0
  28. data/lib/screenkit/callout/styles/default.rb +144 -0
  29. data/lib/screenkit/callout/styles/inline_block.rb +123 -0
  30. data/lib/screenkit/callout/text_style.rb +44 -0
  31. data/lib/screenkit/callout.rb +98 -0
  32. data/lib/screenkit/cli/base.rb +24 -0
  33. data/lib/screenkit/cli/episode.rb +78 -0
  34. data/lib/screenkit/cli/root.rb +73 -0
  35. data/lib/screenkit/cli.rb +9 -0
  36. data/lib/screenkit/config/base.rb +51 -0
  37. data/lib/screenkit/config/episode.rb +42 -0
  38. data/lib/screenkit/config/project.rb +47 -0
  39. data/lib/screenkit/content_type.rb +12 -0
  40. data/lib/screenkit/core_ext/json.rb +47 -0
  41. data/lib/screenkit/core_ext/string.rb +28 -0
  42. data/lib/screenkit/exporter/demotape.rb +26 -0
  43. data/lib/screenkit/exporter/episode.rb +565 -0
  44. data/lib/screenkit/exporter/image.rb +31 -0
  45. data/lib/screenkit/exporter/intro.rb +261 -0
  46. data/lib/screenkit/exporter/outro.rb +183 -0
  47. data/lib/screenkit/exporter/segment.rb +258 -0
  48. data/lib/screenkit/exporter/video.rb +33 -0
  49. data/lib/screenkit/generators/episode/config.yml.erb +106 -0
  50. data/lib/screenkit/generators/episode/content/001.tape +4 -0
  51. data/lib/screenkit/generators/episode/scripts/001.txt +1 -0
  52. data/lib/screenkit/generators/episode.rb +31 -0
  53. data/lib/screenkit/generators/project/Gemfile.erb +7 -0
  54. data/lib/screenkit/generators/project/resources/backtracks/default.aac +0 -0
  55. data/lib/screenkit/generators/project/resources/fonts/opensans/OFL.txt +93 -0
  56. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Bold.ttf +0 -0
  57. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-BoldItalic.ttf +0 -0
  58. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-ExtraBold.ttf +0 -0
  59. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-ExtraBoldItalic.ttf +0 -0
  60. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Italic.ttf +0 -0
  61. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Light.ttf +0 -0
  62. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-LightItalic.ttf +0 -0
  63. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Medium.ttf +0 -0
  64. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-MediumItalic.ttf +0 -0
  65. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Regular.ttf +0 -0
  66. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-SemiBold.ttf +0 -0
  67. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-SemiBoldItalic.ttf +0 -0
  68. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Bold.ttf +0 -0
  69. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-BoldItalic.ttf +0 -0
  70. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBold.ttf +0 -0
  71. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBoldItalic.ttf +0 -0
  72. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Italic.ttf +0 -0
  73. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Light.ttf +0 -0
  74. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-LightItalic.ttf +0 -0
  75. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Medium.ttf +0 -0
  76. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-MediumItalic.ttf +0 -0
  77. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Regular.ttf +0 -0
  78. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBold.ttf +0 -0
  79. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBoldItalic.ttf +0 -0
  80. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Bold.ttf +0 -0
  81. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-BoldItalic.ttf +0 -0
  82. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBold.ttf +0 -0
  83. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBoldItalic.ttf +0 -0
  84. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Italic.ttf +0 -0
  85. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Light.ttf +0 -0
  86. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-LightItalic.ttf +0 -0
  87. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Medium.ttf +0 -0
  88. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-MediumItalic.ttf +0 -0
  89. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Regular.ttf +0 -0
  90. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBold.ttf +0 -0
  91. data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  92. data/lib/screenkit/generators/project/resources/fonts/opensans/README.txt +100 -0
  93. data/lib/screenkit/generators/project/resources/images/logo.png +0 -0
  94. data/lib/screenkit/generators/project/resources/images/watermark.png +0 -0
  95. data/lib/screenkit/generators/project/resources/sounds/chime.mp3 +0 -0
  96. data/lib/screenkit/generators/project/resources/sounds/pop.mp3 +0 -0
  97. data/lib/screenkit/generators/project/resources/sounds/whoosh.mp3 +0 -0
  98. data/lib/screenkit/generators/project/screenkit.yml +189 -0
  99. data/lib/screenkit/generators/project.rb +34 -0
  100. data/lib/screenkit/logfile.rb +33 -0
  101. data/lib/screenkit/parallel_processor.rb +50 -0
  102. data/lib/screenkit/path_lookup.rb +27 -0
  103. data/lib/screenkit/resources/mute.mp3 +0 -0
  104. data/lib/screenkit/resources/transparent.png +0 -0
  105. data/lib/screenkit/schema_validator.rb +14 -0
  106. data/lib/screenkit/schemas/callouts/default.json +44 -0
  107. data/lib/screenkit/schemas/callouts/inline_block.json +20 -0
  108. data/lib/screenkit/schemas/episode.json +74 -0
  109. data/lib/screenkit/schemas/project.json +37 -0
  110. data/lib/screenkit/schemas/refs/anchor.json +17 -0
  111. data/lib/screenkit/schemas/refs/animation.json +7 -0
  112. data/lib/screenkit/schemas/refs/background.json +14 -0
  113. data/lib/screenkit/schemas/refs/callout.json +27 -0
  114. data/lib/screenkit/schemas/refs/color.json +7 -0
  115. data/lib/screenkit/schemas/refs/directory.json +20 -0
  116. data/lib/screenkit/schemas/refs/intro.json +30 -0
  117. data/lib/screenkit/schemas/refs/logo.json +26 -0
  118. data/lib/screenkit/schemas/refs/outro.json +26 -0
  119. data/lib/screenkit/schemas/refs/position.json +38 -0
  120. data/lib/screenkit/schemas/refs/scenes.json +27 -0
  121. data/lib/screenkit/schemas/refs/size.json +19 -0
  122. data/lib/screenkit/schemas/refs/sound.json +36 -0
  123. data/lib/screenkit/schemas/refs/spacing.json +22 -0
  124. data/lib/screenkit/schemas/refs/text_style.json +18 -0
  125. data/lib/screenkit/schemas/refs/transition.json +15 -0
  126. data/lib/screenkit/schemas/refs/tts.json +47 -0
  127. data/lib/screenkit/schemas/refs/watermark.json +18 -0
  128. data/lib/screenkit/schemas/tts/elevenlabs.json +67 -0
  129. data/lib/screenkit/schemas/tts/say.json +16 -0
  130. data/lib/screenkit/shell.rb +58 -0
  131. data/lib/screenkit/sound.rb +44 -0
  132. data/lib/screenkit/spacing.rb +23 -0
  133. data/lib/screenkit/spinner.rb +39 -0
  134. data/lib/screenkit/transition.rb +16 -0
  135. data/lib/screenkit/tts/eleven_labs.rb +51 -0
  136. data/lib/screenkit/tts/say.rb +31 -0
  137. data/lib/screenkit/utils.rb +87 -0
  138. data/lib/screenkit/version.rb +5 -0
  139. data/lib/screenkit/watermark.rb +34 -0
  140. data/lib/screenkit.rb +3 -0
  141. data/screenkit.gemspec +56 -0
  142. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScreenKit
4
+ module CLI
5
+ def self.start(args = ARGV)
6
+ Root.start(args)
7
+ end
8
+ end
9
+ 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