screenkit 0.0.5 → 0.0.7

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.
@@ -177,6 +177,10 @@ module ScreenKit
177
177
  log_path:
178
178
  end
179
179
 
180
+ def script_content
181
+ @script_content ||= script_path.read if script_path.file?
182
+ end
183
+
180
184
  def create_voiceover(log_path:)
181
185
  return if voiceover_path&.file? && !episode.options.overwrite
182
186
  return unless script_path.file?
@@ -185,7 +189,7 @@ module ScreenKit
185
189
  FileUtils.mkdir_p(voiceover_path.dirname)
186
190
 
187
191
  episode.tts_engine.generate(
188
- text: script_path.read,
192
+ text: script_content,
189
193
  output_path: voiceover_path,
190
194
  log_path:
191
195
  )
@@ -294,9 +298,9 @@ module ScreenKit
294
298
  "#{prefix}-#{index}.{png,#{ContentType.video.join(',')}}"
295
299
  ).first
296
300
 
297
- raise "Callout file not found for #{prefix}-#{index}" unless callout_path
301
+ return callout_path if callout_path
298
302
 
299
- callout_path
303
+ raise "Callout file not found for #{prefix}-#{index}"
300
304
  end
301
305
 
302
306
  def video_callout?(callout_path)
@@ -9,8 +9,12 @@ module ScreenKit
9
9
  # The path to the input video file.
10
10
  attr_reader :input_path
11
11
 
12
- def initialize(input_path:)
12
+ # The path to the log file.
13
+ attr_reader :log_path
14
+
15
+ def initialize(input_path:, log_path: nil)
13
16
  @input_path = input_path
17
+ @log_path = log_path
14
18
  end
15
19
 
16
20
  def export(output_path)
@@ -26,7 +30,8 @@ module ScreenKit
26
30
  "-r", "24",
27
31
  "-c:v", "libx264",
28
32
  "-y",
29
- output_path
33
+ output_path,
34
+ log_path:
30
35
  end
31
36
  end
32
37
  end
@@ -1,106 +1,11 @@
1
1
  ---
2
2
  # yaml-language-server: $schema=https://screenkit.dev/schemas/episode.json
3
3
 
4
- # The episode title. When there are no linebreaks, they will be inferred based
5
- # on the approximate text width.
6
- title: "CREATING SCREENCASTS WITH SCREENKIT"
7
-
8
- # Callouts are informational boxes that on the screen during the
9
- # video (also known as lower thirds).
10
- #
11
- # The project's configuration can define the callout structure, while the
12
- # episode configuration can define the content and timing.
13
- callouts_styles:
14
- - type: info
15
- title: "ScreenKit"
16
- body: "Visit https://github.com/fnando/screenkit to learn more."
17
- starts_at: 3
18
- duration: 5
19
- width: 600
20
-
21
- # The background music to be played during the episode.
22
- # When not set, the backtrack will be defined by using the project's
23
- # `backtrack_dir` configuration, and a track will be randomly selected.
24
- # To disable the backtrack entirely, use `backtrack: false`.
25
- # backtrack: resources/backtrack.mp3
26
-
27
- # Define episode-specific scene configurations.
28
- # When present, this will have higher precedence over the project's
29
- # configuration, so you can have different intro/outro for each episode.
30
- # scenes:
31
- # intro:
32
- # # The duration of the intro scene in seconds.
33
- # duration: 5.5
34
- #
35
- # # The fade-in duration in seconds.
36
- # fade_in: 0
37
- #
38
- # # The fade-out duration in seconds.
39
- # fade_out: 0.5
40
- #
41
- # # The background color/image of the intro scene.
42
- # background: "#100f50"
43
- #
44
- # # The title text to be displayed in the intro scene.
45
- # title:
46
- # x: 100
47
- # y: 300
48
- # font_path: open-sans/OpenSans-ExtraBold.ttf
49
- # size: 144
50
- # color: "#ffffff"
51
- #
52
- # # The logo to be displayed in the video intro.
53
- # # Works best with a transparent PNG file in high resolution (at least 2x the
54
- # # size it'll be displayed).
55
- # logo:
56
- # path: images/logo.png
57
- # x: 100
58
- # y: 200
59
- # width: 200
60
- # height: 34
61
- #
62
- # # The sound to be played along with the logo in the intro.
63
- # # This must correspond to a sound file in the sounds directory, with any
64
- # # audio extension (e.g. mp3, m4a, wav).
65
- # sound: sounds/chime.mp3
66
- #
67
- # outro:
68
- # # The duration of the outro scene in seconds.
69
- # duration: 3.5
70
- #
71
- # # The fade-in duration in seconds.
72
- # fade_in: 0.5
73
- #
74
- # # The fade-out duration in seconds.
75
- # fade_out: 0.5
76
- #
77
- # # The background color/image of the outro scene.
78
- # background: "#100f50"
79
- #
80
- # # The sound to be played along with the logo in the outro.
81
- # sound: sounds/chime.mp3
82
- #
83
- # # The logo to be displayed in the video outro.
84
- # logo:
85
- # path: images/logo.png
86
- # x: center
87
- # y: center
88
- # width: 500
89
- #
90
- # segment:
91
- # # The duration of the crossover transition between segments in seconds.
92
- # crossfade_duration: 0.5
93
-
94
- # Define episode-specific voice configuration.
95
- # When present, this will have higher precedence over the project's
96
- # configuration, so you can have different intro/outro segments for each
97
- # episode.
98
- # tts:
99
- # engine: elevenlabs
100
- # voice_id: 56AoDkrOh6qfVPDXZ7Pt
101
- # language_code: en
102
- # voice_settings:
103
- # speed: 0.9
104
- # stability: 0.5
105
- # similarity: 0.75
106
- # style: 0.0
4
+ # The episode title (displayed in intro scene).
5
+ # Line breaks are inferred based on approximate text width.
6
+ title: <%= options.title.to_yaml[4..] %>
7
+
8
+ # Episode-specific overrides for global configuration.
9
+ # Use an editor with JSON Schema support to see available options and
10
+ # autocomplete.
11
+ # Schema: https://screenkit.dev/schemas/episode.json
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: Screenkit
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ inputs:
7
+ episode:
8
+ type: string
9
+ description: "Episode number (e.g. 001)"
10
+ required: true
11
+ tts_preset:
12
+ description: "TTS preset name"
13
+ default: ""
14
+ type: choice
15
+ options:
16
+ - ""
17
+ - say
18
+ - espeak
19
+ - eleven_labs
20
+ - eleven_labs_mp3_192k
21
+ match:
22
+ type: string
23
+ description: "Match pattern for segments (e.g. 001)"
24
+ default: ""
25
+ overwrite:
26
+ type: boolean
27
+ description: "Regenerate files"
28
+
29
+ jobs:
30
+ screenkit:
31
+ runs-on: ubuntu-latest
32
+ permissions:
33
+ contents: read
34
+ actions: write
35
+
36
+ steps:
37
+ - name: Checkout repository
38
+ uses: actions/checkout@v3
39
+
40
+ - name: Run Screenkit
41
+ uses: fnando/screenkit@v1
42
+ with:
43
+ episode: ${{ github.event.inputs.episode }}
44
+ tts_preset: ${{ github.event.inputs.tts_preset }}
45
+ tts_api_key: ${{ secrets.TTS_API_KEY }}
46
+ overwrite: ${{ github.event.inputs.overwrite }}
47
+ match: ${{ github.event.inputs.match }}
48
+ github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -84,23 +84,51 @@ watermark:
84
84
  # Each TTS engine has its own detection mechanism. For instance, say and espeak
85
85
  # checks for a binary with the same name. ElevenLabs checks for the presence of
86
86
  # `--tts-api-key`.
87
+ #
88
+ # You can have multiple presets for the same engine. Just set a different
89
+ # configuration block using a different `id`. Then, when you're exporting the
90
+ # video, you can use `--tts-preset <id>`. If you don't provide a preset, the
91
+ # first available engine will be used.
87
92
  tts:
88
93
  # Apple Say TTS engine configuration.
89
- - engine: say
94
+ - id: say
95
+ engine: say
96
+ rate: 150
97
+ enabled: true
98
+
99
+ # Apple Say TTS engine configuration.
100
+ - id: say_pt_br
101
+ engine: say
102
+ voice: "Luciana"
90
103
  rate: 150
91
104
  enabled: true
92
105
 
93
106
  # eSpeak TTS engine configuration.
94
- - engine: espeak
107
+ - id: espeak
108
+ engine: espeak
95
109
  rate: 150
96
110
  voice: en-us
97
111
  enabled: true
98
112
 
99
113
  # Eleven Labs TTS engine configuration.
100
- - engine: eleven_labs
114
+ - id: eleven_labs
115
+ engine: eleven_labs
116
+ enabled: true
117
+ voice_id: 56AoDkrOh6qfVPDXZ7Pt
118
+ language_code: en
119
+ voice_settings:
120
+ speed: 0.9
121
+ stability: 0.5
122
+ similarity: 0.75
123
+ style: 0.0
124
+
125
+ # Eleven Labs TTS engine configuration.
126
+ - id: eleven_labs_mp3_192k
127
+ engine: eleven_labs
101
128
  enabled: true
102
129
  voice_id: 56AoDkrOh6qfVPDXZ7Pt
103
130
  language_code: en
131
+ output_format: mp3_44100_192
104
132
  voice_settings:
105
133
  speed: 0.9
106
134
  stability: 0.5
@@ -22,6 +22,7 @@ module ScreenKit
22
22
  def copy_files
23
23
  copy_file "screenkit.yml"
24
24
  directory "resources", exclude_pattern: /DS_Store/
25
+ directory ".github", exclude_pattern: /DS_Store/
25
26
  end
26
27
 
27
28
  def bundle_install
@@ -32,6 +33,12 @@ module ScreenKit
32
33
  end
33
34
  end
34
35
 
36
+ def instructions
37
+ cmd = set_color("screenkit episode new --title TITLE", :blue)
38
+ path = set_color(destination_root, :blue)
39
+ say "\nTo create a new episode, run #{cmd} from #{path}"
40
+ end
41
+
35
42
  no_commands do
36
43
  # Add helper methods here
37
44
  end
@@ -7,9 +7,13 @@
7
7
  { "$ref": "../tts/elevenlabs.json" },
8
8
  {
9
9
  "type": "object",
10
- "required": ["engine"],
10
+ "required": ["engine", "id"],
11
11
  "additionalProperties": true,
12
12
  "properties": {
13
+ "id": {
14
+ "type": "string",
15
+ "description": "A unique identifier for the tts configuration"
16
+ },
13
17
  "engine": {
14
18
  "type": "string",
15
19
  "description": "The TTS engine to use",
@@ -5,6 +5,10 @@
5
5
  "type": "object",
6
6
  "required": ["voice_id"],
7
7
  "properties": {
8
+ "id": {
9
+ "type": "string",
10
+ "description": "A unique identifier for the tts configuration"
11
+ },
8
12
  "enabled": {
9
13
  "type": "boolean"
10
14
  },
@@ -4,6 +4,10 @@
4
4
  "title": "espeak synthesizer for English and other languages",
5
5
  "type": "object",
6
6
  "properties": {
7
+ "id": {
8
+ "type": "string",
9
+ "description": "A unique identifier for the tts configuration"
10
+ },
7
11
  "enabled": {
8
12
  "type": "boolean"
9
13
  },
@@ -4,6 +4,10 @@
4
4
  "title": "The Apple `say` voice engine options",
5
5
  "type": "object",
6
6
  "properties": {
7
+ "id": {
8
+ "type": "string",
9
+ "description": "A unique identifier for the tts configuration"
10
+ },
7
11
  "enabled": {
8
12
  "type": "boolean"
9
13
  },
@@ -20,9 +20,9 @@ module ScreenKit
20
20
  !`which #{command}`.strip.empty?
21
21
  end
22
22
 
23
- def run_command(command, *args, log_path: nil)
23
+ def run_command(command, *args, log_path: nil, **)
24
24
  args = args.flatten.compact.map(&:to_s)
25
- stdout, stderr, status = Open3.capture3(command, *args)
25
+ stdout, stderr, status = Open3.capture3(command, *args, **)
26
26
  exit_code = status.exitstatus
27
27
 
28
28
  if exit_code&.nonzero?
@@ -3,18 +3,48 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class Base
6
+ require_relative "../core_ext"
7
+ require_relative "../schema_validator"
8
+
6
9
  extend SchemaValidator
7
10
 
11
+ using CoreExt
12
+
8
13
  # Additional options for the tts engine.
9
14
  attr_reader :options
10
15
 
11
- def initialize(enabled: true, **options)
12
- @enabled = enabled
16
+ # The preset name for the tts engine.
17
+ attr_reader :id
18
+
19
+ # The list of segments.
20
+ # This is available so that engines can contextually generate audio, for
21
+ # instance, by providing previous/next text (e.g. Eleven Labs).
22
+ attr_reader :segments
23
+
24
+ # The API key for the tts engine, if applicable.
25
+ attr_reader :api_key
26
+
27
+ # Detects if the tts engine is available.
28
+ def self.available?(**)
29
+ false
30
+ end
31
+
32
+ def self.engine_name
33
+ name.split("::").last.underscore
34
+ end
35
+
36
+ def initialize(id: nil, segments: nil, api_key: nil, **options)
37
+ @segments = Array(segments)
13
38
  @options = options
39
+ @id = id
40
+ @api_key = api_key
14
41
  end
15
42
 
16
- def enabled?
17
- @enabled
43
+ def redact_file(path, text)
44
+ return unless File.file?(path)
45
+
46
+ content = File.read(path).gsub(text, "[REDACTED]")
47
+ File.write(path, content)
18
48
  end
19
49
  end
20
50
  end
@@ -8,21 +8,16 @@ module ScreenKit
8
8
  .join("screenkit/schemas/tts/elevenlabs.json")
9
9
  end
10
10
 
11
- # The Eleven Labs API key.
12
- attr_reader :api_key
13
-
14
- def initialize(api_key:, **)
15
- super(**)
16
- @api_key = api_key
11
+ def self.available?(api_key: nil, **)
12
+ api_key.to_s.empty?
17
13
  end
18
14
 
19
- def available?
20
- enabled? && !api_key.to_s.empty?
15
+ def all_texts
16
+ @all_texts ||= segments.map(&:script_content)
21
17
  end
22
18
 
23
19
  def generate(output_path:, text:, log_path: nil)
24
- self.class.validate!(options)
25
- voice_id = options.delete(:voice_id)
20
+ voice_id = options[:voice_id]
26
21
 
27
22
  if log_path
28
23
  File.open(log_path, "w") do |f|
@@ -32,9 +27,23 @@ module ScreenKit
32
27
 
33
28
  require "aitch"
34
29
 
30
+ Aitch.configure do |config|
31
+ config.logger = Logger.new(log_path) if log_path
32
+ end
33
+
34
+ current_index = all_texts.index { it == text }
35
+
36
+ if current_index
37
+ previous_text = all_texts[current_index - 1]
38
+ next_text = all_texts[current_index + 1]
39
+ end
40
+
41
+ params = options.merge(text:, previous_text:, next_text:)
42
+ .except(:voice_id)
43
+
35
44
  response = Aitch.post(
36
45
  url: "https://api.elevenlabs.io/v1/text-to-speech/#{voice_id}",
37
- body: JSON.dump(options.merge(text:)),
46
+ body: JSON.dump(params),
38
47
  options: {expect: 200},
39
48
  headers: {
40
49
  "content-type": "application/json",
@@ -44,6 +53,8 @@ module ScreenKit
44
53
  )
45
54
 
46
55
  File.binwrite(output_path, response.body)
56
+ ensure
57
+ redact_file(log_path, api_key)
47
58
  end
48
59
  end
49
60
  end
@@ -3,14 +3,14 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class Espeak < Base
6
- include Shell
6
+ extend Shell
7
7
 
8
- def self.schema_path
9
- ScreenKit.root_dir.join("screenkit/schemas/tts/espeak.json")
8
+ def self.available?(**)
9
+ command_exist?("espeak")
10
10
  end
11
11
 
12
- def available?
13
- enabled? && command_exist?("espeak")
12
+ def self.schema_path
13
+ ScreenKit.root_dir.join("screenkit/schemas/tts/espeak.json")
14
14
  end
15
15
 
16
16
  def generate(text:, output_path:, log_path: nil)
@@ -18,12 +18,12 @@ module ScreenKit
18
18
 
19
19
  {voice: nil, rate: nil}.merge(options) => {voice:, rate:}
20
20
 
21
- run_command "espeak",
22
- (["-v", voice] if voice),
23
- (["-s", rate] if rate),
24
- "-w", output_path.sub_ext(".wav"),
25
- text,
26
- log_path:
21
+ self.class.run_command "espeak",
22
+ (["-v", voice] if voice),
23
+ (["-s", rate] if rate),
24
+ "-w", output_path.sub_ext(".wav"),
25
+ text,
26
+ log_path:
27
27
  end
28
28
  end
29
29
  end
@@ -3,14 +3,14 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class Say < Base
6
- include Shell
6
+ extend Shell
7
7
 
8
8
  def self.schema_path
9
9
  ScreenKit.root_dir.join("screenkit/schemas/tts/say.json")
10
10
  end
11
11
 
12
- def available?
13
- enabled? && command_exist?("say")
12
+ def self.available?(**)
13
+ command_exist?("say")
14
14
  end
15
15
 
16
16
  def generate(text:, output_path:, log_path: nil)
@@ -18,12 +18,12 @@ module ScreenKit
18
18
 
19
19
  {voice: nil, rate: nil}.merge(options) => {voice:, rate:}
20
20
 
21
- run_command "say",
22
- (["-v", voice] if voice),
23
- (["-r", rate] if rate),
24
- "-o", output_path.sub_ext(".aiff"),
25
- text,
26
- log_path:
21
+ self.class.run_command "say",
22
+ (["-v", voice] if voice),
23
+ (["-r", rate] if rate),
24
+ "-o", output_path.sub_ext(".aiff"),
25
+ text,
26
+ log_path:
27
27
  end
28
28
  end
29
29
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ScreenKit
4
4
  module Utils
5
- def has_audio?(path)
5
+ def has_audio?(path) # rubocop:disable Naming/PredicatePrefix
6
6
  cmd = "ffprobe -v error -select_streams a:0 -show_entries " \
7
7
  "stream=codec_type -of default=noprint_wrappers=1:nokey=1"
8
8
  `#{cmd} #{path}`.strip == "audio"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScreenKit
4
- VERSION = "0.0.5"
4
+ VERSION = "0.0.7"
5
5
  end
data/lib/screenkit.rb CHANGED
@@ -1,3 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "screen_kit"
3
+ require "thor"
4
+ require "thor/completion"
5
+ require "yaml"
6
+ require "json-schema"
7
+ require "mini_magick"
8
+ require "pathname"
9
+ require "tty-spinner"
10
+ require "etc"
11
+ require "securerandom"
12
+ require "demo_tape/duration"
13
+
14
+ module ScreenKit
15
+ require_relative "screenkit/version"
16
+ require_relative "screenkit/duration"
17
+ require_relative "screenkit/core_ext"
18
+ require_relative "screenkit/content_type"
19
+ require_relative "screenkit/anchor"
20
+ require_relative "screenkit/banner"
21
+ require_relative "screenkit/time_formatter"
22
+ require_relative "screenkit/spacing"
23
+ require_relative "screenkit/watermark"
24
+ require_relative "screenkit/spinner"
25
+ require_relative "screenkit/shell"
26
+ require_relative "screenkit/schema_validator"
27
+ require_relative "screenkit/generators/project"
28
+ require_relative "screenkit/generators/episode"
29
+ require_relative "screenkit/config/base"
30
+ require_relative "screenkit/config/project"
31
+ require_relative "screenkit/config/episode"
32
+ require_relative "screenkit/callout"
33
+ require_relative "screenkit/callout/text_style"
34
+ require_relative "screenkit/callout/styles/base"
35
+ require_relative "screenkit/transition"
36
+ require_relative "screenkit/parallel_processor"
37
+ require_relative "screenkit/cli"
38
+ require_relative "screenkit/cli/base"
39
+ require_relative "screenkit/cli/episode"
40
+ require_relative "screenkit/cli/root"
41
+ require_relative "screenkit/animation_filters"
42
+ require_relative "screenkit/tts/base"
43
+ require_relative "screenkit/path_lookup"
44
+ require_relative "screenkit/sound"
45
+ require_relative "screenkit/utils"
46
+ require_relative "screenkit/logfile"
47
+ require_relative "screenkit/exporter/intro"
48
+ require_relative "screenkit/exporter/outro"
49
+ require_relative "screenkit/exporter/demotape"
50
+ require_relative "screenkit/exporter/episode"
51
+ require_relative "screenkit/exporter/segment"
52
+ require_relative "screenkit/exporter/image"
53
+ require_relative "screenkit/exporter/video"
54
+
55
+ require_files = lambda do |pattern|
56
+ Gem.find_files_from_load_path(pattern).each do |path|
57
+ next if path.include?("test")
58
+
59
+ require(path)
60
+ end
61
+ end
62
+
63
+ # Load all files that may be available as plugins.
64
+ require_files.call("screenkit/callout/styles/*.rb")
65
+ require_files.call("screenkit/tts/*.rb")
66
+
67
+ def self.root_dir
68
+ @root_dir ||= Pathname(__dir__)
69
+ end
70
+
71
+ # Raised when the configuration schema is invalid.
72
+ InvalidConfigSchemaError = Class.new(StandardError)
73
+
74
+ # Raised when a file is not found.
75
+ FileNotFoundError = Class.new(StandardError)
76
+
77
+ # Raised when a file entry is not found in the lookup.
78
+ FileEntryNotFoundError = Class.new(StandardError)
79
+ end